This commit is contained in:
2026-05-11 03:27:21 +03:30
parent cf7cbb937c
commit d0e68a1a56
854 changed files with 102985 additions and 76 deletions
Submodule
+1
Submodule Accsess added at 13b7643ed3
Submodule
+1
Submodule Ai added at 17628f503f
+521
View File
@@ -0,0 +1,521 @@
# پیشنهاد معماری جدید: Backend-owned Plants + AI farm_data Sync
## خلاصه تصمیم
تصمیم جدید این است که مالک اصلی داده‌های گیاه در سیستم، `Backend` باشد و اپ `plants` در Backend به‌صورت کامل مسئول نگهداری catalog گیاه‌ها شود.
در این معماری:
- اپ `Backend/plants` منبع اصلی `Plant Catalog` خواهد بود
- اپ `Backend/farm_hub` رابطه‌ی فارم با گیاه‌ها را نگه می‌دارد
- اپ `Ai/farm_data` داده‌های مورد نیاز AI را از Backend دریافت می‌کند
- پس از هر تغییر در Backend، داده‌های مرتبط در `Ai/farm_data` به‌روزرسانی می‌شوند
- هرجایی در AI که به اطلاعات گیاه نیاز دارد، از داده‌های `farm_data` استفاده می‌کند
---
## هدف اصلی
این تغییر برای حل چند مسئله انجام می‌شود:
- متمرکز شدن catalog گیاه‌ها در Backend
- حذف پراکندگی ownership داده‌های گیاه
- واضح شدن source of truth
- ساده‌تر شدن استفاده‌ی ماژول‌های AI از اطلاعات گیاه
- فراهم شدن یک read-model مشخص در `Ai/farm_data`
---
## تصمیم domain-level
### 1) Backend مالک اصلی داده‌ی گیاه است
در این معماری، اطلاعات پایه‌ی گیاه باید در Backend نگهداری شود:
- نام گیاه
- مشخصات کاتالوگ
- ویژگی‌های رشد
- نور
- آب
- خاک
- دما
- فصل کاشت
- زمان برداشت
- spacing
- fertilizer
- و هر داده‌ی canonical دیگر
### 2) AI مصرف‌کننده‌ی داده‌ی گیاه است
در این معماری، AI دیگر owner داده‌های گیاه نیست، بلکه از Backend این اطلاعات را دریافت می‌کند.
### 3) `Ai/farm_data` لایه‌ی داده‌ای مورد استفاده‌ی AI می‌شود
در AI، داده‌ی گیاه مستقیماً از مدل canonical مستقل خوانده نمی‌شود، بلکه از داده‌هایی استفاده می‌شود که در `Ai/farm_data` ذخیره یا sync شده‌اند.
---
## ساختار پیشنهادی Backend
## اپ `plants`
اپ `Backend/plants` باید به اپ canonical برای catalog گیاه‌ها تبدیل شود.
### مسئولیت‌ها
- نگهداری لیست همه‌ی گیاه‌ها
- نگهداری اطلاعات catalog هر گیاه
- ارائه‌ی API برای خواندن catalog گیاه‌ها
- ارائه‌ی API برای دریافت جزئیات یک گیاه
- در صورت نیاز، ارائه‌ی endpointهای تغییرات برای sync با AI
### داده‌های این app
نمونه داده‌هایی که بهتر است در `plants` باشند:
- `id`
- `name`
- `slug`
- `icon`
- `description`
- `light`
- `watering`
- `soil`
- `temperature`
- `growth_stage_defaults`
- `planting_season`
- `harvest_time`
- `spacing`
- `fertilizer`
- `health_profile`
- `irrigation_profile`
- `growth_profile`
- `is_active`
- `updated_at`
---
## اپ `farm_hub`
در `Backend/farm_hub` باید رابطه‌ی فارم با گیاه نگهداری شود.
### پیشنهاد رابطه
در گام اول، چیزی که گفتی منطقی است:
- یک رابطه‌ی `ManyToMany` بین `FarmHub` و `Plant`
مثلاً در سطح مفهومی:
```python
class FarmHub(models.Model):
plants = models.ManyToManyField("plants.Plant", related_name="farms", blank=True)
```
این ساختار برای شروع خوب است اگر فقط بخواهی بدانی:
- هر فارم چه گیاه‌هایی دارد
### نکته تکمیلی مهم
اگر بعداً برای رابطه‌ی فارم و گیاه metadata خواستی، `ManyToMany` ساده کافی نیست.
مثلاً اگر این داده‌ها لازم شوند:
- تاریخ کاشت
- وضعیت فعلی رشد
- نوع کشت در آن فارم
- تنظیمات اختصاصی آبیاری
- health state
- مقدار هدف تولید
بهتر است بعداً رابطه به مدل واسط تبدیل شود:
```python
class FarmPlant(models.Model):
farm = models.ForeignKey(FarmHub, on_delete=models.CASCADE)
plant = models.ForeignKey("plants.Plant", on_delete=models.CASCADE)
planted_at = models.DateField(null=True, blank=True)
stage = models.CharField(max_length=64, blank=True)
```
### نتیجه
- برای فاز اول: `ManyToMany` ساده قابل قبول است
- برای فاز matureتر: `through model` بهتر است
---
## ساختار پیشنهادی AI
## نقش `Ai/farm_data`
در این تصمیم جدید، `Ai/farm_data` تبدیل به لایه‌ای می‌شود که داده‌های گیاه و داده‌های فارم-گیاه را برای مصرف ماژول‌های AI نگه می‌دارد.
یعنی:
- Backend source of truth است
- `Ai/farm_data` read/update replica برای use caseهای AI است
### وظایف `farm_data`
- دریافت داده‌ی گیاه از Backend
- دریافت relationهای فارم و گیاه از Backend
- ذخیره‌ی داده‌ی مورد نیاز برای AI
- به‌روزرسانی داده بعد از هر تغییر
- ارائه‌ی data access ساده برای سایر appهای AI
---
## جریان داده پیشنهادی
### مرحله 1: تغییر در Backend
هر تغییری که در این بخش‌ها رخ می‌دهد:
- ایجاد گیاه جدید
- ویرایش catalog گیاه
- حذف یا غیرفعال‌سازی گیاه
- تغییر گیاه‌های مرتبط با یک فارم
باید باعث شود `Ai/farm_data` نیز به‌روزرسانی شود.
### مرحله 2: sync به AI
بعد از تغییر، Backend از طریق API یا مکانیزم sync، داده را به `Ai/farm_data` می‌رساند.
### مرحله 3: مصرف در AI
هر app در AI که به داده‌ی گیاه نیاز دارد، به‌جای dependency مستقیم روی مدل‌های قدیمی یا منابع پراکنده، از `farm_data` می‌خواند.
---
## نکته مهم درباره `apps.py`
اگرچه در پیام قبلی بحث `Ai/farm_data/apps.py` مطرح بود، در این تصمیم جدید بهتر است منطق sync اصلی در `apps.py` قرار نگیرد.
### `apps.py` برای چه مناسب است؟
- app registration
- signal registration
- import سبک startup
### `apps.py` برای چه مناسب نیست؟
- orchestration سنگین sync
- HTTP fetch logic
- retry logic
- reconciliation logic
- business update flow
### پیشنهاد بهتر
منطق sync بهتر است در این فایل‌ها قرار بگیرد:
- `Ai/farm_data/services.py`
- `Ai/farm_data/tasks.py`
- `Ai/farm_data/clients/backend.py`
و `apps.py` فقط wiring را انجام دهد.
---
## دو نوع داده‌ای که باید تفکیک شوند
برای اینکه این معماری بعداً تمیز بماند، بهتر است از همین ابتدا دو نوع داده را از هم جدا ببینی.
### 1) Plant Catalog
داده‌های عمومی و canonical هر گیاه:
- نام
- مشخصات رشد
- پروفایل آبیاری
- نیازهای محیطی
این داده‌ها متعلق به `Backend/plants` هستند.
### 2) Farm Plant Assignment / Context
داده‌هایی که مشخص می‌کنند:
- یک فارم چه گیاهی دارد
- چه گیاهی برای آن فارم فعال است
- چه context یا تنظیمی روی آن اعمال شده
این داده‌ها از relation بین فارم و گیاه می‌آیند و در Backend تعریف می‌شوند، اما برای مصرف AI در `farm_data` sync می‌شوند.
---
## چه appهایی در AI باید update شوند؟
در این معماری جدید، همه appهای AI لزوماً نیاز به تغییر مستقیم ندارند؛ فقط appهایی که الان مستقیم یا غیرمستقیم به داده‌ی گیاه یا `SensorData.plants` وابسته‌اند باید update شوند.
### 1) `Ai/farm_data`
این app مهم‌ترین app برای تغییر است و باید به هسته‌ی integration جدید تبدیل شود.
#### فایل‌ها و بخش‌هایی که باید update شوند
- `Ai/farm_data/models.py`
- `Ai/farm_data/serializers.py`
- `Ai/farm_data/services.py`
- `Ai/farm_data/views.py`
- `Ai/farm_data/urls.py`
- `Ai/farm_data/apps.py`
- `Ai/farm_data/management/commands/seed_farm_data.py`
- `Ai/farm_data/tests/`
#### تغییرات مورد نیاز
- حذف وابستگی مستقیم `ManyToMany` به `plant.Plant` در صورت جایگزینی با replica data
- تعریف ساختار جدید برای نگهداری `plant catalog snapshot` یا `farm plant snapshot`
- افزودن service client برای خواندن data از Backend
- افزودن endpoint یا task برای sync داده از Backend
- تغییر serializerها تا payload جدید گیاه را برگردانند
- اصلاح تست‌ها بر اساس source جدید داده
#### نکته مهم
الان در `Ai/farm_data/models.py` فیلد `plants` به `plant.Plant` وصل است. این نقطه یکی از اصلی‌ترین جاهایی است که باید بازطراحی شود، چون در معماری جدید AI نباید برای catalog گیاه به مدل canonical داخلی تکیه کند.
---
### 2) `Ai/rag`
این app از مهم‌ترین مصرف‌کننده‌های اطلاعات گیاه است و باید update شود تا به‌جای مدل‌های قبلی، از `farm_data` بخواند.
#### فایل‌ها و بخش‌هایی که باید update شوند
- `Ai/rag/user_data.py`
- `Ai/rag/services/irrigation.py`
- `Ai/rag/services/fertilization.py`
- `Ai/rag/services/water_need_prediction.py`
- `Ai/rag/services/pest_disease.py`
- `Ai/rag/services/yield_harvest.py`
- `Ai/rag/tests/test_recommendation_services.py`
- هر فایل دیگری که مستقیم `Plant` یا `SensorData.plants` را می‌خواند
#### تغییرات مورد نیاز
- حذف import مستقیم از `plant.models`
- استفاده از facade یا service داخل `farm_data` برای دریافت plant context
- جایگزینی readهای مستقیم از مدل گیاه با read model جدید
- اصلاح تست‌ها بر اساس structure جدید داده
#### نکته مهم
در حال حاضر `Ai/rag/user_data.py` مستقیم به `Plant` وابستگی دارد. این وابستگی باید حذف شود و همه چیز از `farm_data` خوانده شود.
---
### 3) `Ai/irrigation`
این app احتمالاً از داده‌ی گیاه برای recommendation یا indicator استفاده می‌کند و باید با read model جدید سازگار شود.
#### فایل‌ها و بخش‌هایی که باید update شوند
- `Ai/irrigation/indicators.py`
- `Ai/irrigation/views.py`
- تست‌های `irrigation`
#### تغییرات مورد نیاز
- اگر منطق آبیاری به plant profile وابسته است، profile باید از `farm_data` خوانده شود
- حذف هر dependency متنی یا مستقیم به جدول `Plant`
- هماهنگی responseها با data contract جدید
---
### 4) `Ai/soile`
این app مستقیماً `SensorData` را می‌خواند و در queryها `plants` را prefetch می‌کند؛ بنابراین باید update شود.
#### فایل‌ها و بخش‌هایی که باید update شوند
- `Ai/soile/services.py`
- `Ai/soile/test_soil_moisture_heatmap_api.py`
#### تغییرات مورد نیاز
- بازنویسی queryهایی که `prefetch_related("plants")` دارند
- استفاده از plant data جدیدی که در `farm_data` ذخیره می‌شود
- اصلاح منطق‌هایی که فرض می‌کنند `plants` یک relation مستقیم ORM به مدل `Plant` است
---
### 5) `Ai/weather`
این app بیشتر weather-centric است، ولی برخی flowها از `SensorData` و در مواردی از context فارم/گیاه برای پیش‌بینی نیاز آبی استفاده می‌کنند.
#### فایل‌ها و بخش‌هایی که باید update شوند
- `Ai/weather/water_need_prediction.py`
- `Ai/weather/farm_weather.py`
- تست‌های مرتبط با farm weather
#### تغییرات مورد نیاز
- اگر plant context برای water need استفاده می‌شود، منبع آن باید `farm_data` باشد
- اگر serializer یا service فرض می‌کند relation قبلی `plants` برقرار است، باید اصلاح شود
---
### 6) `Ai/location_data`
این app بیشتر location-centric است و وابستگی مستقیم شدیدی به گیاه ندارد، اما چون با `SensorData` کار می‌کند باید از سازگاری model جدید مطمئن شویم.
#### فایل‌ها و بخش‌هایی که باید update شوند
- `Ai/location_data/ndvi.py`
- `Ai/location_data/views.py` در صورت استفاده از farm context
- تست‌های location مرتبط با farm
#### تغییرات مورد نیاز
- بیشتر در حد compatibility check با ساختار جدید `farm_data`
- اگر plant-aware response وجود دارد، باید با source جدید هماهنگ شود
---
### 7) `Ai/farm_alerts`
اگر alertها به نوع گیاه، stage، یا context گیاه متکی باشند، این app هم باید update شود.
#### فایل‌ها و بخش‌هایی که باید بررسی/آپدیت شوند
- `Ai/farm_alerts/services.py`
- `Ai/farm_alerts/alerts_tracker.py`
- `Ai/farm_alerts/views.py`
#### تغییرات مورد نیاز
- هرجا alert rule به plant info وابسته است، plant info باید از `farm_data` خوانده شود
- اگر فعلاً وابستگی مستقیم ندارد، فقط compatibility review کافی است
---
### 8) `Ai/economy`
این app ممکن است در بعضی سناریوها به نوع گیاه برای تحلیل اقتصادی وابسته شود.
#### فایل‌ها و بخش‌هایی که باید بررسی شوند
- `Ai/economy/services.py`
- `Ai/economy/views.py`
- تست‌های economy
#### تغییرات مورد نیاز
- اگر نوع گیاه یا catalog گیاه در محاسبات اقتصادی استفاده می‌شود، باید از `farm_data` خوانده شود
- اگر فعلاً استفاده‌ای ندارد، تنها review کافی است
---
## appهایی که احتمالاً بیشترین تغییر را دارند
اگر بخواهیم اولویت‌بندی کنیم، بیشترین تغییر در AI به‌ترتیب در این appها خواهد بود:
- `Ai/farm_data`
- `Ai/rag`
- `Ai/soile`
- `Ai/irrigation`
- `Ai/weather`
و این appها بیشتر نیاز به review و compatibility check دارند:
- `Ai/location_data`
- `Ai/farm_alerts`
- `Ai/economy`
---
## الگوی پیشنهادی برای جلوگیری از پخش شدن وابستگی‌ها
برای اینکه تغییرات در AI کنترل‌پذیر بماند، بهتر است appهای دیگر مستقیماً model جدید `farm_data` را تکه‌تکه query نزنند.
### پیشنهاد
یک لایه‌ی access مرکزی در `farm_data` تعریف شود، مثلاً:
- `Ai/farm_data/services.py`
- یا `Ai/farm_data/context.py`
و appهای دیگر فقط از همین interface استفاده کنند.
### مثال از مسئولیت این facade
- گرفتن plant catalog برای یک farm
- گرفتن primary plant یا active plants
- گرفتن irrigation profile گیاه
- گرفتن growth/health context گیاه
این کار باعث می‌شود اگر بعداً schema داخلی `farm_data` عوض شد، فقط یک لایه update شود.
---
## تغییرات مستندی که باید در AI انجام شوند
علاوه بر کد، این بخش‌های مستنداتی هم باید update شوند:
- `Ai/API_REFERENCE_FA.md`
- `Ai/APPS_URLS_AUDIT.md`
- `Ai/API_RELIABILITY_AUDIT_FA.md`
- هر doc مرتبط با `Plant API`
- هر doc مرتبط با `farm_data` payload
### دلیل
چون در وضعیت جدید:
- `Plant API` دیگر نباید به‌عنوان canonical داخل AI معرفی شود
- `farm_data` باید به‌عنوان plant consumer/read model معرفی شود
- endpointها و schemaهای response احتمالاً تغییر می‌کنند
---
## پیشنهاد فازبندی update اپ‌های AI
### فاز 1: هسته‌ی data layer
- update `Ai/farm_data`
- تعریف schema جدید data
- تعریف sync service با Backend
### فاز 2: مصرف‌کننده‌های اصلی
- update `Ai/rag`
- update `Ai/soile`
- update `Ai/irrigation`
- update `Ai/weather`
### فاز 3: مصرف‌کننده‌های ثانویه
- review/update `Ai/location_data`
- review/update `Ai/farm_alerts`
- review/update `Ai/economy`
### فاز 4: پاک‌سازی نهایی
- حذف dependency مستقیم به `plant.models`
- حذف endpointهای قدیمی یا deprecated در AI
- اصلاح تست‌ها و docs
---
## صورت‌بندی نهایی این تغییر
نسخه‌ی دقیق‌تر و کامل‌تر این تصمیم به این صورت است:
- یک app کامل `plants` در Backend نگهدارنده‌ی catalog همه‌ی گیاه‌ها باشد.
- در `Backend/farm_hub` رابطه‌ی `ManyToMany` بین `FarmHub` و `Plant` تعریف شود.
- بعد از هر تغییر در catalog گیاه یا رابطه‌ی فارم-گیاه، داده‌ها به `Ai/farm_data` sync شوند.
- `Ai/farm_data` به منبع مصرفی همه‌ی ماژول‌های AI برای اطلاعات گیاه تبدیل شود.
- در AI هرجا اطلاعات گیاه، catalog گیاه، یا نوع گیاه لازم است، از `farm_data` خوانده شود، نه از مدل‌های پراکنده یا منبع دیگری.
- appهای AI که الان به `Plant` یا `SensorData.plants` وابسته‌اند باید مرحله‌به‌مرحله به این مدل جدید مهاجرت کنند.
---
## نتیجه یک‌خطی
این تصمیم از نظر معماری قابل دفاع و قابل توسعه است، به شرطی که `Backend/plants` source of truth بماند، `farm_hub` رابطه‌ی فارم-گیاه را نگه دارد، `Ai/farm_data` لایه‌ی sync/read برای AI باشد، و تمام appهای وابسته در AI به‌صورت کنترل‌شده از dependency مستقیم به `Plant` جدا شوند.
Submodule
+1
Submodule Backend added at 35f4d09225
+58
View File
@@ -0,0 +1,58 @@
# CropLogic Authorization Service
This service runs OPA as a standalone authorization engine for `backend/access_control`.
## Run standalone
```bash
docker compose -f accsess/docker-compose.yaml up -d
```
If you want request logging only on development, start the stack with
`APP_ENV=DEVELOP` and enable the `develop` profile. In that mode, OPA sends
decision logs to a sidecar service, and the log file is written to
`accsess/logs/opa.log` on the host through a Docker volume.
```bash
APP_ENV=DEVELOP COMPOSE_PROFILES=develop docker compose -f accsess/docker-compose.yaml up -d
```
## Decision endpoints
- Single feature: `POST /v1/data/croplogic/authz/decision`
- Batch features: `POST /v1/data/croplogic/authz/batch_decision`
The backend uses the batch endpoint and sends the farm context only. Users are treated as `farmer` by default inside the service, and features are allowed unless there is a feature-specific rule in `policies/authz.rego`.
## Example request
```bash
curl -s http://127.0.0.1:8181/v1/data/croplogic/authz/batch_decision \
-H 'Content-Type: application/json' \
-d @- <<'EOF'
{
"input": {
"resource": {
"farm_id": "farm-1001",
"subscription_plan_codes": ["gold"],
"farm_types": ["greenhouse"],
"crop_types": ["tomato"],
"cultivation_types": ["soil"],
"sensor_codes": ["sensor-7-in-1"],
"power_sensor": ["main-power"],
"customization": ["default-layout"]
},
"features": ["sensor-7-in-1"],
"action": "view"
}
}
EOF
```
## Add new rules in code
Define feature-specific checks directly in `policies/authz.rego`.
- If a feature has no rule, every action is allowed.
- If a feature rule exists, its conditions are evaluated and any failing condition denies access.
- `sensor-7-in-1` currently requires `resource.sensor_codes` to include one of the supported 7-in-1 sensor codes (`sensor-7-in-1` or `sensor_7_soil_moisture_sensor_v1_2`).
@@ -0,0 +1,11 @@
services:
requestlog:
url: http://opa-log-receiver:8282/logs
labels:
app: croplogic-authz
plugins: {}
decision_logs:
service: requestlog
reporting:
min_delay_seconds: 1
max_delay_seconds: 5
@@ -0,0 +1,4 @@
services: {}
labels:
app: croplogic-authz
plugins: {}
+4
View File
@@ -0,0 +1,4 @@
services: {}
labels:
app: croplogic-authz
plugins: {}
+43
View File
@@ -0,0 +1,43 @@
services:
opa:
image: mirror-docker.runflare.com/openpolicyagent/opa
container_name: croplogic-accsess-opa
command:
- run
- --server
- --addr=0.0.0.0:8181
- --config-file=/config/opa-config.${APP_ENV:-default}.yaml
- /policies
environment:
APP_ENV: ${APP_ENV:-}
ports:
- "8181:8181"
volumes:
- ./policies:/policies:ro
- ./config/opa-config.default.yaml:/config/opa-config.default.yaml:ro
- ./config/opa-config.DEVELOP.yaml:/config/opa-config.DEVELOP.yaml:ro
restart: unless-stopped
networks:
- crop_network
opa-log-receiver:
image: docker.iranserver.com/python:3.10
container_name: croplogic-accsess-opa-log-receiver
profiles:
- develop
command:
- python
- /app/scripts/opa_log_receiver.py
environment:
OPA_REQUEST_LOG_FILE: /logs/opa.log
OPA_REQUEST_LOG_PORT: "8282"
volumes:
- ./scripts/opa_log_receiver.py:/app/scripts/opa_log_receiver.py:ro
- ./logs:/logs
restart: unless-stopped
networks:
- crop_network
networks:
crop_network:
external: true
+1
View File
@@ -0,0 +1 @@
+45
View File
@@ -0,0 +1,45 @@
{"timestamp": "2026-04-09T20:07:30.617741+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "401", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "c211530e-d6bb-4067-abed-57fe193b6e5b", "version": "1.15.2"}, "decision_id": "3ee2fa07-ce00-4c79-9782-76edae020652", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["feature1", "feature2", "feature3"]}, "result": {"features": {"feature1": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}, "feature2": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}, "feature3": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.1:59682", "timestamp": "2026-04-09T20:07:29.762128957Z", "metrics": {"counter_server_query_cache_hit": 0, "timer_rego_input_parse_ns": 57527, "timer_rego_query_compile_ns": 93329, "timer_rego_query_eval_ns": 199196, "timer_server_handler_ns": 471175}, "req_id": 1}]}
{"timestamp": "2026-04-09T20:08:21.624001+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "544", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "c211530e-d6bb-4067-abed-57fe193b6e5b", "version": "1.15.2"}, "decision_id": "b6ca9264-4576-4826-ac70-067ad6850019", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_management"], "resource": {"crop_types": [], "cultivation_types": [], "customization": [], "farm_id": null, "farm_types": [], "power_sensor": [], "sensor_codes": [], "subscription_plan_codes": []}, "route": "/api/farm-hub/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_management": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:35524", "timestamp": "2026-04-09T20:08:19.762624801Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 71833, "timer_rego_query_eval_ns": 127252, "timer_server_handler_ns": 231704}, "req_id": 2}]}
{"timestamp": "2026-04-09T20:08:43.385941+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "621", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "c211530e-d6bb-4067-abed-57fe193b6e5b", "version": "1.15.2"}, "decision_id": "b52a1754-aaf8-4c7d-9bfb-1d25d803f007", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:41130", "timestamp": "2026-04-09T20:08:43.104063998Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 83718, "timer_rego_query_eval_ns": 141627, "timer_server_handler_ns": 263982}, "req_id": 3}]}
{"timestamp": "2026-04-09T20:12:48.961473+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "625", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "c211530e-d6bb-4067-abed-57fe193b6e5b", "version": "1.15.2"}, "decision_id": "98adca8f-68fb-47d6-8162-59c2cb83d18b", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_management"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-hub/11111111-1111-1111-1111-111111111111/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_management": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:46450", "timestamp": "2026-04-09T20:12:47.941138139Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 97369, "timer_rego_query_eval_ns": 181108, "timer_server_handler_ns": 317548}, "req_id": 4}]}
{"timestamp": "2026-04-09T20:42:43.646161+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "821", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "4db76a9f-a2b6-44a0-b1f9-cb75f9133d39", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["notifications"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/notifications/long-poll/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"notifications": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:37396", "timestamp": "2026-04-09T20:42:41.927602811Z", "metrics": {"counter_server_query_cache_hit": 0, "timer_rego_input_parse_ns": 122530, "timer_rego_query_compile_ns": 132430, "timer_rego_query_eval_ns": 194996, "timer_server_handler_ns": 592073}, "req_id": 1}, {"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "43b88797-57cd-4f31-90ba-c237e2a3714e", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:37408", "timestamp": "2026-04-09T20:42:41.927600148Z", "metrics": {"counter_server_query_cache_hit": 0, "timer_rego_input_parse_ns": 108570, "timer_rego_query_compile_ns": 97471, "timer_rego_query_eval_ns": 192309, "timer_server_handler_ns": 523163}, "req_id": 2}, {"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "407f0ba2-f6df-4baa-9fa7-407791970cab", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard-config/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:37416", "timestamp": "2026-04-09T20:42:41.931016741Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 73967, "timer_rego_query_eval_ns": 108157, "timer_server_handler_ns": 209895}, "req_id": 3}]}
{"timestamp": "2026-04-09T20:47:47.651969+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "624", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "b3ad986c-eb28-444f-b1e8-c0f493b45c57", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["notifications"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/notifications/long-poll/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"notifications": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:41406", "timestamp": "2026-04-09T20:47:45.947059039Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 147422, "timer_rego_query_eval_ns": 241268, "timer_server_handler_ns": 449636}, "req_id": 4}]}
{"timestamp": "2026-04-09T20:48:18.700793+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "711", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "b3a6098e-fb0c-47fa-be19-1c2c68e0669a", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard-config/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:48310", "timestamp": "2026-04-09T20:48:18.354416572Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 225867, "timer_rego_query_eval_ns": 432224, "timer_server_handler_ns": 756714}, "req_id": 5}, {"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "58bd4844-fc59-4465-96d3-e6aa53b40874", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:48314", "timestamp": "2026-04-09T20:48:18.373055564Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 201358, "timer_rego_query_eval_ns": 282319, "timer_server_handler_ns": 555958}, "req_id": 6}]}
{"timestamp": "2026-04-09T20:50:09.747146+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "627", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "680c9a39-8be5-4419-85cc-becc0010888d", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["sensor_external_api"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/sensor-external-api/logs/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"sensor_external_api": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:48356", "timestamp": "2026-04-09T20:50:07.630315688Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 190621, "timer_rego_query_eval_ns": 377687, "timer_server_handler_ns": 636782}, "req_id": 7}]}
{"timestamp": "2026-04-09T20:53:00.381383+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "623", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "1cbf7a8c-5704-4ccb-b4a9-2d0a73f56311", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["notifications"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/notifications/long-poll/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"notifications": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:38240", "timestamp": "2026-04-09T20:52:57.471075759Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 202996, "timer_rego_query_eval_ns": 412022, "timer_server_handler_ns": 676636}, "req_id": 8}]}
{"timestamp": "2026-04-09T20:58:03.913608+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "624", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "b8307e1a-be3c-4b2e-b81f-371e06d6ba84", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["notifications"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/notifications/long-poll/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"notifications": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:40004", "timestamp": "2026-04-09T20:58:01.30297863Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 428967, "timer_rego_query_eval_ns": 640043, "timer_server_handler_ns": 2784581}, "req_id": 9}]}
{"timestamp": "2026-04-09T21:01:31.740737+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "828", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "1057c499-26c7-4621-abc9-c1831fd58171", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["crop_zoning"], "resource": {"crop_types": [], "cultivation_types": [], "customization": [], "farm_id": null, "farm_types": [], "power_sensor": [], "sensor_codes": [], "subscription_plan_codes": []}, "route": "/api/crop-zoning/products/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"crop_zoning": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:50406", "timestamp": "2026-04-09T21:01:29.230461946Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 189901, "timer_rego_query_eval_ns": 437429, "timer_server_handler_ns": 743792}, "req_id": 10}, {"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "f5b638e4-4192-4629-8a73-18a61af2a239", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:50420", "timestamp": "2026-04-09T21:01:29.288269547Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 4126050, "timer_rego_query_eval_ns": 447210, "timer_server_handler_ns": 4687167}, "req_id": 11}, {"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "257de564-b48d-4ad4-8eff-d4cfb08f4b45", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["crop_zoning"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/crop-zoning/area/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"crop_zoning": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:50434", "timestamp": "2026-04-09T21:01:29.305181512Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 292984, "timer_rego_query_eval_ns": 382137, "timer_server_handler_ns": 852898}, "req_id": 12}]}
{"timestamp": "2026-04-09T21:02:40.904920+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "634", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "5b9f103a-0da8-4f97-a9d3-5d51aa7706b1", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["irrigation_recommendation"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/irrigation-recommendation/config/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"irrigation_recommendation": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:60326", "timestamp": "2026-04-09T21:02:39.279986375Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 225891, "timer_rego_query_eval_ns": 434331, "timer_server_handler_ns": 734656}, "req_id": 13}]}
{"timestamp": "2026-04-09T21:02:54.426343+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "625", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "937aff32-1b0c-46b0-b11a-cac657eba503", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard-config/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:49500", "timestamp": "2026-04-09T21:02:54.24022169Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 115636, "timer_rego_query_eval_ns": 157180, "timer_server_handler_ns": 315565}, "req_id": 14}]}
{"timestamp": "2026-04-09T21:03:06.146197+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "623", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "d370fad0-87fb-4c6e-a16c-8ac2e0d6ce37", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["notifications"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/notifications/long-poll/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"notifications": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:58422", "timestamp": "2026-04-09T21:03:05.166520574Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 222441, "timer_rego_query_eval_ns": 434203, "timer_server_handler_ns": 729628}, "req_id": 15}]}
{"timestamp": "2026-04-09T21:04:41.647980+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "627", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "9fa975bf-7165-4cae-afe3-e7ff3cdbfee0", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["sensor_external_api"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/sensor-external-api/logs/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"sensor_external_api": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:36278", "timestamp": "2026-04-09T21:04:40.019161288Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 179885, "timer_rego_query_eval_ns": 383747, "timer_server_handler_ns": 659616}, "req_id": 16}]}
{"timestamp": "2026-04-09T21:08:09.965162+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "626", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "c72f1740-9674-47a3-a23b-80cc7a655932", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["notifications"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/notifications/long-poll/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"notifications": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:48524", "timestamp": "2026-04-09T21:08:08.897327932Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 137304, "timer_rego_query_eval_ns": 294343, "timer_server_handler_ns": 496201}, "req_id": 17}]}
{"timestamp": "2026-04-09T21:13:15.332070+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "625", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "a79b8136-54cd-42bb-8d2e-64741f066d21", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["notifications"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/notifications/long-poll/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"notifications": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:56696", "timestamp": "2026-04-09T21:13:12.582212863Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 188803, "timer_rego_query_eval_ns": 311352, "timer_server_handler_ns": 579955}, "req_id": 18}]}
{"timestamp": "2026-04-09T21:18:17.324979+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "625", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "ba2cb809-35b1-4bce-8d39-acae0fc6ed17", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["notifications"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/notifications/long-poll/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"notifications": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:39430", "timestamp": "2026-04-09T21:18:16.023067852Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 187659, "timer_rego_query_eval_ns": 326240, "timer_server_handler_ns": 598344}, "req_id": 19}]}
{"timestamp": "2026-04-09T21:23:21.107040+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "625", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "3e590f06-dcd8-4e76-aa25-073f379c411a", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["notifications"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/notifications/long-poll/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"notifications": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:33694", "timestamp": "2026-04-09T21:23:19.61493343Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 216489, "timer_rego_query_eval_ns": 343120, "timer_server_handler_ns": 640460}, "req_id": 20}]}
{"timestamp": "2026-04-09T21:23:51.096118+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "634", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "f36d69af-6c86-41b3-9184-692ae5757e0b", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["fertilization_recommendation"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/fertilization-recommendation/config/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"fertilization_recommendation": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:46050", "timestamp": "2026-04-09T21:23:50.4594069Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 141471, "timer_rego_query_eval_ns": 253595, "timer_server_handler_ns": 441400}, "req_id": 21}]}
{"timestamp": "2026-04-09T21:23:59.638974+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "618", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "90f17cae-9c75-477b-8e6e-62f10747c178", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:39476", "timestamp": "2026-04-09T21:23:59.139056735Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 78302, "timer_rego_query_eval_ns": 131142, "timer_server_handler_ns": 239624}, "req_id": 22}]}
{"timestamp": "2026-04-09T21:24:13.834987+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "625", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "22b2fea3-df6f-4c67-8f5d-635018b768ce", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard-config/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:44008", "timestamp": "2026-04-09T21:24:13.491294566Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 99303, "timer_rego_query_eval_ns": 133459, "timer_server_handler_ns": 269374}, "req_id": 23}]}
{"timestamp": "2026-04-09T21:24:24.386767+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "634", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "bc769ac1-110c-4e5b-876e-3c97614d23e2", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["irrigation_recommendation"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/irrigation-recommendation/config/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"irrigation_recommendation": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:60398", "timestamp": "2026-04-09T21:24:22.957633923Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 132389, "timer_rego_query_eval_ns": 273816, "timer_server_handler_ns": 458756}, "req_id": 24}]}
{"timestamp": "2026-04-09T21:24:26.123960+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "717", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "45c886c5-6915-4f66-8128-d5cd7a55782a", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_ai_assistant"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-ai-assistant/chats/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_ai_assistant": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:60404", "timestamp": "2026-04-09T21:24:25.0271866Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 85483, "timer_rego_query_eval_ns": 139055, "timer_server_handler_ns": 260195}, "req_id": 25}, {"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "aedc4325-7aff-4e18-87f4-7d55eda31778", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_ai_assistant"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-ai-assistant/context/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_ai_assistant": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:60418", "timestamp": "2026-04-09T21:24:25.027670559Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 107674, "timer_rego_query_eval_ns": 193988, "timer_server_handler_ns": 335440}, "req_id": 26}]}
{"timestamp": "2026-04-09T21:24:34.050416+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "548", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "ae431b9a-9b28-4925-941f-6301c30fd500", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["crop_zoning"], "resource": {"crop_types": [], "cultivation_types": [], "customization": [], "farm_id": null, "farm_types": [], "power_sensor": [], "sensor_codes": [], "subscription_plan_codes": []}, "route": "/api/crop-zoning/products/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"crop_zoning": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:33316", "timestamp": "2026-04-09T21:24:30.974139448Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 168178, "timer_rego_query_eval_ns": 301097, "timer_server_handler_ns": 551443}, "req_id": 27}]}
{"timestamp": "2026-04-09T21:25:10.553432+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "627", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "35d98c65-5a5b-40c3-aacb-8bbb6ce99eb3", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["sensor_external_api"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/sensor-external-api/logs/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"sensor_external_api": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:60564", "timestamp": "2026-04-09T21:25:07.821441729Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 118614, "timer_rego_query_eval_ns": 171169, "timer_server_handler_ns": 333953}, "req_id": 28}]}
{"timestamp": "2026-04-09T21:28:24.705585+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "627", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "2f0f70da-c1ee-46b7-9d9b-f2b05069f646", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["notifications"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/notifications/long-poll/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"notifications": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:36142", "timestamp": "2026-04-09T21:28:23.492387976Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 163958, "timer_rego_query_eval_ns": 395973, "timer_server_handler_ns": 626870}, "req_id": 29}]}
{"timestamp": "2026-04-09T21:33:28.926444+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "626", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "041e89b6-4d30-4209-93d4-6873c932e769", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["notifications"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/notifications/long-poll/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"notifications": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:45426", "timestamp": "2026-04-09T21:33:27.151055419Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 191209, "timer_rego_query_eval_ns": 362144, "timer_server_handler_ns": 641233}, "req_id": 30}]}
{"timestamp": "2026-04-09T21:45:19.312225+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "624", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "124be292-ce82-463f-82ef-7fe214970e29", "path": "croplogic/authz/batch_decision", "input": {"action": "edit", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard-config/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:54864", "timestamp": "2026-04-09T21:45:16.835047241Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 79343, "timer_rego_query_eval_ns": 124097, "timer_server_handler_ns": 233847}, "req_id": 31}]}
{"timestamp": "2026-04-09T22:08:21.946179+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "623", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "9f6d085f-80fa-4a44-b17d-faf6cec36a29", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:51558", "timestamp": "2026-04-09T22:08:21.419844497Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 209725, "timer_rego_query_eval_ns": 304029, "timer_server_handler_ns": 596478}, "req_id": 32}]}
{"timestamp": "2026-04-09T22:13:25.931508+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "551", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "163bde65-a50e-4d71-9b61-244b21609523", "version": "1.15.2"}, "decision_id": "ce83dc1c-2bce-41ba-b4f1-6400625b21c3", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["pest_detection"], "resource": {"crop_types": [], "cultivation_types": [], "customization": [], "farm_id": null, "farm_types": [], "power_sensor": [], "sensor_codes": [], "subscription_plan_codes": []}, "route": "/api/pest-detection/risk-summary/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"pest_detection": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:46430", "timestamp": "2026-04-09T22:13:23.71054823Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 114520, "timer_rego_query_eval_ns": 313737, "timer_server_handler_ns": 478644}, "req_id": 33}]}
{"timestamp": "2026-04-10T12:59:27.063813+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "633", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "dc58e9b5-a435-409a-93f1-71f10cb3133d", "version": "1.15.2"}, "decision_id": "cf7b20b1-28fa-47d9-995b-511b31430498", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.8:50840", "timestamp": "2026-04-10T12:59:26.376629473Z", "metrics": {"counter_server_query_cache_hit": 0, "timer_rego_input_parse_ns": 82394, "timer_rego_query_compile_ns": 80078, "timer_rego_query_eval_ns": 180560, "timer_server_handler_ns": 466938}, "req_id": 1}]}
{"timestamp": "2026-04-10T13:00:53.951689+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "625", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "dc58e9b5-a435-409a-93f1-71f10cb3133d", "version": "1.15.2"}, "decision_id": "6d1c7b00-69ed-4999-9c2c-f7bb9ab0727f", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard-config/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.8:51898", "timestamp": "2026-04-10T13:00:52.910682603Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 56363, "timer_rego_query_eval_ns": 104895, "timer_server_handler_ns": 183538}, "req_id": 2}]}
{"timestamp": "2026-04-10T13:05:35.880304+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "728", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "dc58e9b5-a435-409a-93f1-71f10cb3133d", "version": "1.15.2"}, "decision_id": "79bd4af9-b001-4a32-9ebd-ae9644b5ed5b", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["notifications"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/notifications/long-poll/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"notifications": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.8:36464", "timestamp": "2026-04-10T13:05:34.042562699Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 113161, "timer_rego_query_eval_ns": 213810, "timer_server_handler_ns": 366862}, "req_id": 3}, {"labels": {"app": "croplogic-authz", "id": "dc58e9b5-a435-409a-93f1-71f10cb3133d", "version": "1.15.2"}, "decision_id": "d42028ac-6b0e-4e54-b1d2-88cdce949db2", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.8:36470", "timestamp": "2026-04-10T13:05:34.042718253Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 124025, "timer_rego_query_eval_ns": 160477, "timer_server_handler_ns": 328769}, "req_id": 4}]}
{"timestamp": "2026-04-10T13:10:26.348263+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "625", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "dc58e9b5-a435-409a-93f1-71f10cb3133d", "version": "1.15.2"}, "decision_id": "aa2d1d08-5f17-4ee3-8c90-6e2928bc7cf9", "path": "croplogic/authz/batch_decision", "input": {"action": "edit", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard-config/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.8:33764", "timestamp": "2026-04-10T13:10:24.047412541Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 91827, "timer_rego_query_eval_ns": 174690, "timer_server_handler_ns": 301470}, "req_id": 5}]}
{"timestamp": "2026-04-10T19:35:13.527185+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "804", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "dc58e9b5-a435-409a-93f1-71f10cb3133d", "version": "1.15.2"}, "decision_id": "22c0e059-5faa-4f9a-9157-eaeb62acbb2b", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["notifications"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/notifications/long-poll/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"notifications": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.8:54296", "timestamp": "2026-04-10T19:35:11.403480105Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 132185, "timer_rego_query_eval_ns": 213468, "timer_server_handler_ns": 403665}, "req_id": 6}, {"labels": {"app": "croplogic-authz", "id": "dc58e9b5-a435-409a-93f1-71f10cb3133d", "version": "1.15.2"}, "decision_id": "34f5000a-5ab9-496c-a874-cae2d1458dc4", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard-config/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.8:54324", "timestamp": "2026-04-10T19:35:11.403824522Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 101753, "timer_rego_query_eval_ns": 146739, "timer_server_handler_ns": 278383}, "req_id": 8}, {"labels": {"app": "croplogic-authz", "id": "dc58e9b5-a435-409a-93f1-71f10cb3133d", "version": "1.15.2"}, "decision_id": "4f1bb10b-b00a-4dd5-833d-a2aad5157c72", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.8:54312", "timestamp": "2026-04-10T19:35:11.403840867Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 124451, "timer_rego_query_eval_ns": 238400, "timer_server_handler_ns": 405116}, "req_id": 7}]}
{"timestamp": "2026-04-10T19:40:16.822686+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "623", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "dc58e9b5-a435-409a-93f1-71f10cb3133d", "version": "1.15.2"}, "decision_id": "a3d8f2a4-6be8-4f81-866a-6fb7dc64f71d", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["notifications"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/notifications/long-poll/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"notifications": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.8:34800", "timestamp": "2026-04-10T19:40:13.702403965Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 99787, "timer_rego_query_eval_ns": 136786, "timer_server_handler_ns": 270242}, "req_id": 9}]}
{"timestamp": "2026-04-10T19:45:16.830582+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "626", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "dc58e9b5-a435-409a-93f1-71f10cb3133d", "version": "1.15.2"}, "decision_id": "4902a064-067a-4397-9deb-2569c4025f0c", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["notifications"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/notifications/long-poll/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"notifications": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.8:44536", "timestamp": "2026-04-10T19:45:15.905382397Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 59497, "timer_rego_query_eval_ns": 89688, "timer_server_handler_ns": 171264}, "req_id": 10}]}
{"timestamp": "2026-04-10T19:50:21.097707+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "624", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "dc58e9b5-a435-409a-93f1-71f10cb3133d", "version": "1.15.2"}, "decision_id": "9d45ae58-8c15-41d6-bdea-8a2d092f5077", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["notifications"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/notifications/long-poll/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"notifications": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.8:52928", "timestamp": "2026-04-10T19:50:17.987713407Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 80255, "timer_rego_query_eval_ns": 121141, "timer_server_handler_ns": 229313}, "req_id": 11}]}
{"timestamp": "2026-04-10T20:01:13.782745+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "625", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "dc58e9b5-a435-409a-93f1-71f10cb3133d", "version": "1.15.2"}, "decision_id": "d0212f25-f532-4f88-9f16-7ae26a544d32", "path": "croplogic/authz/batch_decision", "input": {"action": "edit", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard-config/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.8:58694", "timestamp": "2026-04-10T20:01:11.127619736Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 62603, "timer_rego_query_eval_ns": 107738, "timer_server_handler_ns": 192904}, "req_id": 12}]}
{"timestamp": "2026-04-10T22:43:38.461548+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "813", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "dc58e9b5-a435-409a-93f1-71f10cb3133d", "version": "1.15.2"}, "decision_id": "b8740cd7-1fe6-48f7-aff1-b39dc90fb193", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.8:33582", "timestamp": "2026-04-10T22:43:38.202921222Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 105707, "timer_rego_query_eval_ns": 206860, "timer_server_handler_ns": 358673}, "req_id": 13}, {"labels": {"app": "croplogic-authz", "id": "dc58e9b5-a435-409a-93f1-71f10cb3133d", "version": "1.15.2"}, "decision_id": "2f64aa8f-698c-4a32-8859-7ce62e44f95f", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard-config/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.8:33590", "timestamp": "2026-04-10T22:43:38.208182493Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 156086, "timer_rego_query_eval_ns": 370544, "timer_server_handler_ns": 591838}, "req_id": 14}, {"labels": {"app": "croplogic-authz", "id": "dc58e9b5-a435-409a-93f1-71f10cb3133d", "version": "1.15.2"}, "decision_id": "a8f45f35-f6b1-4e57-9871-aaa0d9b07ef7", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["notifications"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/notifications/long-poll/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"notifications": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.8:33588", "timestamp": "2026-04-10T22:43:38.210392032Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 126615, "timer_rego_query_eval_ns": 223747, "timer_server_handler_ns": 396115}, "req_id": 15}]}
{"timestamp": "2026-04-10T22:44:04.863487+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "725", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "dc58e9b5-a435-409a-93f1-71f10cb3133d", "version": "1.15.2"}, "decision_id": "6e09a8a2-3a53-4178-86aa-f8a4b674fe93", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["crop_zoning"], "resource": {"crop_types": [], "cultivation_types": [], "customization": [], "farm_id": null, "farm_types": [], "power_sensor": [], "sensor_codes": [], "subscription_plan_codes": []}, "route": "/api/crop-zoning/products/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"crop_zoning": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.8:42600", "timestamp": "2026-04-10T22:44:04.71595662Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 136439, "timer_rego_query_eval_ns": 288419, "timer_server_handler_ns": 479665}, "req_id": 16}, {"labels": {"app": "croplogic-authz", "id": "dc58e9b5-a435-409a-93f1-71f10cb3133d", "version": "1.15.2"}, "decision_id": "bd2bdb46-692e-407b-b90c-c4fa95804f2f", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["crop_zoning"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/crop-zoning/area/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"crop_zoning": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.8:42602", "timestamp": "2026-04-10T22:44:04.760420723Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 163656, "timer_rego_query_eval_ns": 273513, "timer_server_handler_ns": 495728}, "req_id": 17}]}
{"timestamp": "2026-04-10T22:48:43.430769+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "624", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "dc58e9b5-a435-409a-93f1-71f10cb3133d", "version": "1.15.2"}, "decision_id": "fb2914ca-9008-4ece-b5d5-f330cc3116ee", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["notifications"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/notifications/long-poll/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"notifications": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.8:47896", "timestamp": "2026-04-10T22:48:41.674746737Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 151182, "timer_rego_query_eval_ns": 260819, "timer_server_handler_ns": 467222}, "req_id": 18}]}
{"timestamp": "2026-04-10T22:53:45.402384+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "625", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "dc58e9b5-a435-409a-93f1-71f10cb3133d", "version": "1.15.2"}, "decision_id": "ae549db0-12be-4aa1-a39d-9cbfeb4661ce", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["notifications"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/notifications/long-poll/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"notifications": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.8:60402", "timestamp": "2026-04-10T22:53:44.711350785Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 289900, "timer_rego_query_eval_ns": 320890, "timer_server_handler_ns": 687822}, "req_id": 19}]}
{"timestamp": "2026-04-10T22:53:49.440465+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "710", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "dc58e9b5-a435-409a-93f1-71f10cb3133d", "version": "1.15.2"}, "decision_id": "12ee2ea3-ad7d-4567-8aa3-4ac1197d106b", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard-config/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.8:38306", "timestamp": "2026-04-10T22:53:48.92124304Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 98580, "timer_rego_query_eval_ns": 174745, "timer_server_handler_ns": 312667}, "req_id": 21}, {"labels": {"app": "croplogic-authz", "id": "dc58e9b5-a435-409a-93f1-71f10cb3133d", "version": "1.15.2"}, "decision_id": "3644f42e-05e5-478a-a420-cb7eb42d705f", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.8:38304", "timestamp": "2026-04-10T22:53:48.921478862Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 270515, "timer_rego_query_eval_ns": 303118, "timer_server_handler_ns": 648768}, "req_id": 20}]}
+68
View File
@@ -0,0 +1,68 @@
package croplogic.authz
import rego.v1
default allow := false
allow if {
decision.allow
}
decision := feature_decision(input.feature)
batch_decision := {
"features": {
feature: result |
feature := input.features[_]
result := feature_decision(feature)
},
}
feature_decision(feature) := {
"allow": true,
"matched_rules": [],
"deny_rules": [],
"allow_rules": [],
} if {
not has_feature_rule(feature)
}
feature_decision(feature) := result if {
has_feature_rule(feature)
rule := feature_rule(feature)
matched := [matched_rule | matched_rule := rule; action_match(matched_rule)]
deny_rules := [matched_rule | matched_rule := matched[_]; not object.get(matched_rule, "allow", false)]
allow_rules := [matched_rule | matched_rule := matched[_]; object.get(matched_rule, "allow", false)]
count(deny_rules) == 0
result := {
"allow": true,
"matched_rules": matched,
"deny_rules": deny_rules,
"allow_rules": allow_rules,
}
}
feature_decision(feature) := result if {
has_feature_rule(feature)
rule := feature_rule(feature)
matched := [matched_rule | matched_rule := rule; action_match(matched_rule)]
deny_rules := [matched_rule | matched_rule := matched[_]; not object.get(matched_rule, "allow", false)]
allow_rules := [matched_rule | matched_rule := matched[_]; object.get(matched_rule, "allow", false)]
count(deny_rules) > 0
result := {
"allow": false,
"matched_rules": matched,
"deny_rules": deny_rules,
"allow_rules": allow_rules,
}
}
action_match(rule) if {
count(object.get(rule, "actions_any", [])) == 0
}
action_match(rule) if {
requested_action := lower(sprintf("%v", [object.get(input, "action", "view")]))
action := object.get(rule, "actions_any", [])[_]
lower(sprintf("%v", [action])) == requested_action
}
@@ -0,0 +1,3 @@
{
"authz": {}
}
@@ -0,0 +1,48 @@
package croplogic.authz
import rego.v1
has_feature_rule(feature) if {
is_sensor_7_in_1_feature(feature)
}
feature_rule(feature) := {
"code": "sensor-7-in-1-requires-sensor-code",
"allow": true,
"reason": "sensor-7-in-1 feature requires sensor_codes to include a supported 7-in-1 sensor code",
} if {
is_sensor_7_in_1_feature(feature)
has_any_supported_sensor_7_in_1_code
}
feature_rule(feature) := {
"code": "sensor-7-in-1-requires-sensor-code",
"allow": false,
"reason": "sensor-7-in-1 feature requires sensor_codes to include a supported 7-in-1 sensor code",
} if {
is_sensor_7_in_1_feature(feature)
not has_any_supported_sensor_7_in_1_code
}
is_sensor_7_in_1_feature(feature) if {
lower(sprintf("%v", [feature])) == "sensor-7-in-1"
}
has_any_supported_sensor_7_in_1_code if {
supported_code := {"sensor-7-in-1", "sensor_7_soil_moisture_sensor_v1_2"}[_]
has_sensor_code(supported_code)
}
has_sensor_code(code) if {
sensor_codes := object.get(input.resource, "sensor_codes", [])
is_array(sensor_codes)
sensor_code := sensor_codes[_]
lower(sprintf("%v", [sensor_code])) == lower(sprintf("%v", [code]))
}
has_sensor_code(code) if {
sensor_code := object.get(input.resource, "sensor_codes", null)
sensor_code != null
not is_array(sensor_code)
lower(sprintf("%v", [sensor_code])) == lower(sprintf("%v", [code]))
}
@@ -0,0 +1,44 @@
import json
import os
import gzip
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
LOG_FILE = os.environ.get("OPA_REQUEST_LOG_FILE", "/logs/opa.log")
PORT = int(os.environ.get("OPA_REQUEST_LOG_PORT", "8282"))
class DecisionLogHandler(BaseHTTPRequestHandler):
def do_POST(self):
content_length = int(self.headers.get("Content-Length", "0"))
raw_payload = self.rfile.read(content_length) if content_length else b""
content_encoding = self.headers.get("Content-Encoding", "").lower()
if content_encoding == "gzip" and raw_payload:
raw_payload = gzip.decompress(raw_payload)
payload = raw_payload.decode("utf-8") if raw_payload else ""
entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"path": self.path,
"headers": dict(self.headers.items()),
"body": json.loads(payload) if payload else None,
}
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
with open(LOG_FILE, "a", encoding="utf-8") as log_file:
log_file.write(json.dumps(entry, ensure_ascii=True) + "\n")
self.send_response(200)
self.end_headers()
self.wfile.write(b"ok")
def log_message(self, format, *args):
return
if __name__ == "__main__":
server = ThreadingHTTPServer(("0.0.0.0", PORT), DecisionLogHandler)
server.serve_forever()
+74
View File
@@ -0,0 +1,74 @@
---
alwaysApply: false
---
# Backend API Architecture & Postman
## 1. URL / Routing Architecture
- **Root (config/urls.py):** API mounts under `api/<app-prefix>/` via `include()`.
- Example: `path("api/auth/", include("auth.urls"))`, `path("api/sensor-hub/", include("sensor_hub.urls"))`.
- App prefix: kebab-case (e.g. `sensor-hub`).
- **App URLs (each apps urls.py):** Only endpoint definitions with `path()`.
- Same view can be used for several paths; distinguish by path or `kwargs` (e.g. `kwargs={"action": "active"}`).
- Order matters: more specific paths first (e.g. `active/`, `deactive/`), then path-param routes (e.g. `<uuid:uuid>/`), then base `""` for list.
- Example pattern:
- `path("active/", View.as_view(), kwargs={"action": "active"})`
- `path("deactive/", View.as_view(), kwargs={"action": "deactive"})`
- `path("<uuid:uuid>/", View.as_view(), name="...-detail")`
- `path("", View.as_view(), name="...-list")`
- **Views:** One `APIView` per resource (or per flow, e.g. auth). Dispatch by HTTP method and optionally by `request.path` or `kwargs` (e.g. `uuid`, `action`). No business logic in views; orchestration only.
---
## 2. Postman Collection Layout
- **Placement:** One collection per app: `<app_name>/postman/<collection_name>.json` (e.g. `sensor_hub/postman/sensor_hub.json`, `auth/postman/postman.json`).
- **Structure:**
- `info`: `name`, `schema` (v2.1.0), optional `description`.
- `item`: array of requests (one per endpoint variant/method).
- `variable`: at least `baseUrl` (e.g. `http://localhost:8000`); add `token`, `uuid` etc. when needed.
- **Request style:**
- One base URL per resource; multiple requests for different methods or path params (e.g. list vs `{{uuid}}/`).
- URL: `{{baseUrl}}/api/<app-prefix>/...` (e.g. `{{baseUrl}}/api/sensor-hub/`, `{{baseUrl}}/api/sensor-hub/{{uuid}}/`).
- Auth: where required, header `Authorization: Bearer {{token}}`.
- No random/dynamic values in body or response examples.
---
## 3. Postman Request Generator (when I give you routes)
Your task is to take the API routes I provide and convert them into a valid Postman collection JSON (as above).
ROUTE STYLE:
- When routes are defined as a single URL with different HTTP methods, generate one base URL and multiple requests (one per method/variant). Use the same URL for all; use path params (e.g. `<uuid>/`) or query for GET detail vs list when applicable.
RULES:
1. For each route (or each method/variant on the same URL), generate:
- Name: A descriptive, concise name for the request based on the route and HTTP method.
- Method: The HTTP method (GET, POST, PUT, DELETE, etc.).
- URL: The route URL.
- Body: If the endpoint accepts input, provide a JSON body example with appropriate keys; otherwise, leave it empty.
- Response: Provide a sample JSON response in the following format:
- If the endpoint returns no data:
{
"status": "success"
}
- If the endpoint returns data:
{
"status": "success",
"data": {}
}
- All responses must use HTTP status 200.
2. Do NOT generate random or dynamic values in the body or response.
3. Output must be a valid Postman collection JSON structure:
- Include "info" with collection name.
- Include "item" array with all requests.
4. Keep the JSON fully compatible with Postman import.
5. Do NOT include explanations outside the JSON.
Wait for me to provide the route definitions.
+96
View File
@@ -0,0 +1,96 @@
---
alwaysApply: true
---
## 2. Django App (Module) Naming
| Item | Convention | Example |
|--------|------------|----------------------------|
| App name | snake_case, **بدون** پسوند `_api` | `account`, `auth`, `sensor_hub` |
- نام اپ‌ها را با `_api` تمام **نکنید**. مثلاً به‌جای `account_api` از `account` استفاده کنید.
- برای ماژول‌های فقط API، همان نام دامنه کافی است (مثلاً `auth`، `account`).
---
## 3. Model and Database Field Naming
| Item | Convention | Example |
|-------------------|-------------------------|----------------------------------------------|
| Model | PascalCase | `UserProfile` |
| Fields | snake_case | `first_name`, `email_address` |
| Boolean | `is_` / `has_` + name | `is_active`, `has_paid` |
| Date/Time | `created_at` / `updated_at` | `created_at`, `updated_at` |
| ForeignKey / M2M | snake_case, often model name | `author = ForeignKey(UserProfile)` |
| Choices / Enum | UPPER_SNAKE_CASE values | `role = CharField(choices=(("ADMIN","Admin"), ("USER","User")))` |
---
## 4. DRF Conventions
- **Serializer:** Validation + data transformation. Use PascalCase names.
- **Service layer:** All business logic lives here.
- **View:** Orchestration only — call services and return responses. No business logic in views.
- **URLs:** Define endpoints only. Use kebab-case for URL paths.
---
## 5. API Response Format
همه پاسخ‌های API باید فیلد `code` را برگردانند؛ مقدار آن برابر **HTTP status code** درخواست است (مثلاً 200، 201، 400، 404).
| فیلد | توضیح |
|-------|----------------------------------------|
| `code` | کد وضعیت HTTP (مثلاً 200، 404، 500) |
| `msg` | پیام (مثلاً "success" برای 2xx) |
| `data` | دادهٔ برگشتی (اختیاری) |
مثال:
```json
{"code": 200, "msg": "success", "data": {...}}
```
---
## 6. Simple Example: How the Layers Connect (users app)
```python
# models.py
class UserProfile(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
email_address = models.EmailField(unique=True)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# serializers.py
class UserCreateSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfile
fields = ["first_name", "last_name", "email_address"]
# services.py
def create_user(first_name, last_name, email_address):
return UserProfile.objects.create(
first_name=first_name,
last_name=last_name,
email_address=email_address,
)
# views.py
class UserCreateAPIView(APIView):
def post(self, request):
serializer = UserCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = create_user(**serializer.validated_data)
return Response({"code": 201, "msg": "success", "data": {"id": user.id}}, status=201)
```
- All names follow the conventions above.
- Business logic is in `services.py`.
- Serializer only validates and serializes.
- View only orchestrates (calls service, returns response).
+72
View File
@@ -0,0 +1,72 @@
---
alwaysApply: false
---
You are a Django API code generator.
Your task is to generate a complete and runnable Django project based on the routes I provide.
ROUTE STYLE:
- Routes may be defined as a single URL with different HTTP methods (e.g. one path, GET for list, GET with query for detail, PUT/PATCH for update, DELETE for delete, POST for action). Use one view class that implements get, post, put, patch, delete as needed. Use query parameters (e.g. sensor_id) to distinguish list vs detail when both use GET.
STRICT RULES:
- Use Django only.
- Do NOT use Django REST Framework unless I explicitly request it.
- Do NOT connect to any database.
- Do NOT create any Models.
- Do NOT generate random or dynamic data.
- Input parameters must be accepted (body, query params, path params).
- HOWEVER, absolutely NO processing, validation, transformation, or logic may be applied to them.
- Do NOT use input values inside the response.
- No conditional logic.
- No business logic.
- No validation.
- All endpoints must always return static JSON responses only.
- ALL responses must return HTTP status code 200 only.
- No other status codes are allowed.
- No explanations outside the code.
- Return complete runnable code including project structure (views.py, urls.py, settings if needed, etc.).
--------------------------------------------------
RESPONSE FORMAT (STRICTLY ENFORCED)
If the endpoint does NOT require returning data:
{
"status": "success"
}
If the endpoint requires returning data:
{
"status": "success",
"data": {}
}
Mandatory rules:
- The "status" field MUST always be exactly "success".
- If "data" is present, it MUST be exactly an empty object {}.
- If data is not required, DO NOT include the "data" field.
- No additional fields are allowed.
--------------------------------------------------
COMMENTING REQUIREMENTS (VERY IMPORTANT):
Each endpoint MUST include professional, multi-line docstring documentation.
The documentation MUST include:
1. Clear description of the endpoint purpose.
2. Complete description of ALL input parameters:
- Parameter name
- Data type
- Location (body / query / path)
- Description of its intended purpose
3. Full description of the response structure:
- status field
- data field (if applicable)
4. Explicit statement that no processing or validation is performed on inputs.
Use clean, professional API documentation style.
Do not write anything outside the code.
Wait for my route definitions.
+16
View File
@@ -0,0 +1,16 @@
.env
.env.*
!.env.example
.git
__pycache__
*.pyc
.venv
venv
*.egg-info
.pytest_cache
.coverage
htmlcov
*.log
media
staticfiles
.cursor
+34
View File
@@ -0,0 +1,34 @@
# Django
SECRET_KEY=your-secret-key-change-in-production
DEBUG=1
ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0,web
DEVELOP=true
# Database (MySQL)
DB_ENGINE=django.db.backends.mysql
DB_NAME=croplogic
DB_USER=croplogic
DB_PASSWORD=changeme
DB_ROOT_PASSWORD=root
DB_HOST=db
DB_PORT=3306
AVALAI_API_KEY=aa-iDlMpRAfRyd95pCQxr5YXfJoJmw4qCDe6fnozQ4PlkpYF0pA
AVALAI_BASE_URL=https://api.avalai.ir/v1
# GapGPT API (provider اصلی)
GAPGPT_API_KEY=sk-ZeFmDwROcQ2rYOFmUxHLjIwMTSUdo2qNc3Uraug9dOK2ihn5
GAPGPT_BASE_URL=https://api.gapgpt.app/v1
# Weather API (Open-Meteo)
WEATHER_API_BASE_URL=https://api.open-meteo.com/v1/forecast
WEATHER_API_KEY=
# Soil data provider: soilgrids | mock
SOIL_DATA_PROVIDER=soilgrids
SOIL_MOCK_DELAY_SECONDS=0.8
SOILGRIDS_TIMEOUT_SECONDS=60
WEATHER_DATA_PROVIDER=open-meteo
WEATHER_MOCK_DELAY_SECONDS=0.8
WEATHER_TIMEOUT_SECONDS=60
+120
View File
@@ -0,0 +1,120 @@
name: AI Service CI/CD
on:
push:
branches: [production]
paths:
- '**'
- '.gitea/workflows/ai.yml'
pull_request:
branches: [production]
paths:
- '**'
- '.gitea/workflows/ai.yml'
jobs:
test:
name: Lint & Test
runs-on: self-hosted
container:
image: mirror2.chabokan.net/ubuntu:22.04
options: --add-host gitea:172.17.0.1
steps:
- name: Setup Ubuntu apt mirrors
run: |
tee /etc/apt/sources.list > /dev/null <<'EOF'
deb https://mirror-linux.runflare.com/ubuntu/ noble main restricted universe multiverse
deb https://mirror-linux.runflare.com/ubuntu/ noble-updates main restricted universe multiverse
deb https://mirror-linux.runflare.com/ubuntu/ noble-backports main restricted universe multiverse
deb https://mirror-linux.runflare.com/ubuntu/ noble-security main restricted universe multiverse
deb [trusted=yes] https://mirror2.chabokan.net/ubuntu focal main universe
deb [trusted=yes] https://mirror2.chabokan.net/ubuntu focal-updates main universe
deb [trusted=yes] https://mirror2.chabokan.net/ubuntu focal-security main universe
deb http://mirror.iranserver.com/ubuntu/ jammy main restricted
deb-src http://mirror.iranserver.com/ubuntu/ jammy main restricted
deb http://mirror.iranserver.com/ubuntu/ jammy-updates main restricted
deb-src http://mirror.iranserver.com/ubuntu/ jammy-updates main restricted
deb http://mirror.iranserver.com/ubuntu/ jammy universe
deb-src http://mirror.iranserver.com/ubuntu/ jammy universe
deb http://mirror.iranserver.com/ubuntu/ jammy-updates universe
deb http://mirror.iranserver.com/ubuntu/ jammy multiverse
deb-src http://mirror.iranserver.com/ubuntu/ jammy multiverse
deb http://mirror.iranserver.com/ubuntu/ jammy-updates multiverse
deb-src http://mirror.iranserver.com/ubuntu/ jammy-updates multiverse
deb http://mirror.iranserver.com/ubuntu/ jammy-backports main restricted universe multiverse
deb-src http://mirror.iranserver.com/ubuntu/ jammy-backports main restricted universe multiverse
EOF
apt-get update
- name: Install git
run: |
apt-get install -y git
- name: Checkout repository
run: |
git clone http://15f3baa28036aa35f8eb707585567d1b87bd8977@git.crop-logic.ir/sajad-dev/Ai.git .
- name: Install Python
run: |
apt-get install -y python3 python3-pip python3-venv git
- name: Setup Python pip mirrors
run: |
pip3 config --user set global.index-url https://package-mirror.liara.ir/repository/pypi/simple
pip3 config --user set global.extra-index-url https://mirror.cdn.ir/repository/pypi/simple
pip3 config --user set global.trusted-host "package-mirror.liara.ir mirror.cdn.ir mirror2.chabokan.net"
- name: Install system dependencies
run: |
apt-get install -y \
python3 \
python3-pip \
python3-venv \
pkg-config \
build-essential \
default-libmysqlclient-dev
- name: Install dependencies
run: |
python3 -m pip install --upgrade pip
pip3 install -r requirements.txt
pip3 install pytest flake8
- name: Run lint
run: |
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
- name: Setup SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -p ${{secrets.SERVER_SSH_PORT}} -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts
- name: Deploy
run: |
ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} -p ${{secrets.SERVER_SSH_PORT}}<< 'EOF'
cd application/Ai
git pull origin production
docker-compose -f docker-compose-prod.yaml down --remove-orphans
docker-compose -f docker-compose-prod.yaml up -d
EOF
+120
View File
@@ -0,0 +1,120 @@
name: AI Service CI/CD
on:
push:
branches: [main]
paths:
- 'ai/**'
- 'ai/.github/workflows/ai.yml'
pull_request:
branches: [main]
paths:
- 'ai/**'
- 'ai/.github/workflows/ai.yml'
defaults:
run:
working-directory: ai
jobs:
test:
name: Lint & Test
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
python-version: ['3.11']
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Ubuntu apt mirrors
run: |
sudo tee /etc/apt/sources.list > /dev/null <<'EOF'
deb [trusted=yes] https://mirror2.chabokan.net/ubuntu jammy main restricted universe multiverse
deb [trusted=yes] https://mirror2.chabokan.net/ubuntu jammy-updates main restricted universe multiverse
deb [trusted=yes] https://mirror2.chabokan.net/ubuntu jammy-security main restricted universe multiverse
EOF
sudo apt-get update
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Setup Python pip mirrors
run: |
pip config --user set global.index-url https://package-mirror.liara.ir/repository/pypi/simple
pip config --user set global.extra-index-url https://mirror.cdn.ir/repository/pypi/simple
pip config --user set global.trusted-host "package-mirror.liara.ir mirror.cdn.ir mirror2.chabokan.net"
- name: Cache pip dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-ai-${{ hashFiles('ai/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-ai-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest flake8
- name: Run lint
run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
- name: Run tests
run: pytest --tb=short -q
docker:
name: Build & Push Docker Image
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: ${{ secrets.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: ./ai
push: true
tags: |
${{ secrets.DOCKER_REGISTRY }}/ai:latest
${{ secrets.DOCKER_REGISTRY }}/ai:${{ github.sha }}
build-args: |
APT_MIRROR=mirror2.chabokan.net
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
name: Deploy AI Service
needs: docker
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
port: ${{ secrets.SSH_PORT }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /opt/myproject/ai
git pull origin main
docker compose pull
docker compose up -d --remove-orphans
+64
View File
@@ -0,0 +1,64 @@
# Environment
.env
.env.local
*.env
!*.env.example
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
.venv/
venv/
ENV/
env/
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# Django
*.log
local_settings.py
db.sqlite3
media/
staticfiles/
*.pot
# RAG / ChromaDB
data/chromadb/
# Testing / Coverage
.coverage
htmlcov/
.pytest_cache/
.tox/
.nox/
# OS
.DS_Store
Thumbs.db
logs/*
+4
View File
@@ -0,0 +1,4 @@
[submodule "Schemas"]
path = Schemas
url = ssh://git@git.crop-logic.ir:2222/sajad-dev/Schemas.git
branch = develop
File diff suppressed because it is too large Load Diff
+69
View File
@@ -0,0 +1,69 @@
# ممیزی وضعیت واقعی APIها
این سند فقط درباره reliability نیست؛ به‌عنوان یک مرجع فشرده برای `وضعیت واقعی routeها` و semantics فعلی هم استفاده می‌شود.
## قانون runtime در برابر seed
- seed/fixture/bootstrap data مجاز است و باید برای bootstrap، dev و test باقی بماند.
- mock/sample/demo data نباید در runtime application code به عنوان fallback موفق استفاده شود.
- اگر داده واقعی موجود نیست، پاسخ باید `empty state` یا `failure contract` صریح باشد.
## جدول مرجع وضعیت
| Endpoint | وضعیت | semantics | توضیح کوتاه |
|---|---:|---|---|
| `POST /api/rag/chat/` | `implemented` | `live AI` | route واقعی AI |
| `POST /api/farm-alerts/tracker/` | `implemented` | `live AI` | route واقعی AI؛ معادل backend آن cached است |
| `GET|POST /api/soil-data/` | `implemented` | `provider-backed / task-backed` | route واقعی AI |
| `GET /api/soil-data/tasks/{task_id}/status/` | `implemented` | `async status` | route واقعی AI |
| `POST /api/soil-data/ndvi-health/` | `implemented` | `provider-backed` | route واقعی AI |
| `POST /api/soile/*` | `implemented` | `AI-owned derived output` | routeهای واقعی AI |
| `POST /api/farm-data/` | `implemented` | `AI-owned derived write-model` | route واقعی AI |
| `GET /api/farm-data/{farm_uuid}/detail/` | `implemented` | `AI-owned derived read-model` | route واقعی AI |
| `POST /api/farm-data/parameters/` | `implemented` | `AI-owned config` | route واقعی AI |
| `POST /api/weather/farm-card/` | `implemented` | `provider-backed` | route واقعی AI |
| `POST /api/weather/water-need-prediction/` | `implemented` | `derived output` | route واقعی AI |
| `POST /api/economy/overview/` | `implemented` | `provider-backed / persisted` | route واقعی AI |
| `GET|POST /api/plants/` | `implemented` | `canonical AI plant service` | route واقعی AI |
| `GET|PUT|PATCH|DELETE /api/plants/{pk}/` | `implemented` | `canonical AI plant service` | route واقعی AI |
| `POST /api/plants/fetch-info/` | `implemented` | `provider-backed enrichment` | route واقعی AI |
| `POST /api/pest-disease/detect/` | `implemented` | `live AI` | route واقعی AI |
| `POST /api/pest-disease/risk/` | `implemented` | `derived output` | route واقعی AI |
| `GET|POST /api/irrigation/` | `implemented` | `AI-owned config + live recommendation support` | route واقعی AI |
| `GET|PUT|PATCH|DELETE /api/irrigation/{pk}/` | `implemented` | `AI-owned config` | route واقعی AI |
| `POST /api/irrigation/recommend/` | `implemented` | `live AI + deterministic context` | route واقعی AI |
| `POST /api/irrigation/plan-from-text/` | `implemented` | `live AI parsing` | route واقعی AI |
| `POST /api/irrigation/water-stress/` | `implemented` | `AI-owned derived output` | route واقعی AI |
| `POST /api/fertilization/recommend/` | `implemented` | `live AI + optimizer context` | route واقعی AI |
| `POST /api/fertilization/plan-from-text/` | `implemented` | `live AI parsing` | route واقعی AI |
| `POST /api/crop-simulation/current-farm-chart/` | `implemented` | `live AI inference` | route واقعی AI |
| `POST /api/crop-simulation/harvest-prediction/` | `implemented` | `live AI inference` | route واقعی AI |
| `GET /api/crop-simulation/yield-harvest-summary/` | `implemented` | `AI-owned derived output` | route واقعی AI |
| `POST /api/crop-simulation/yield-prediction/` | `implemented` | `live AI inference` | route واقعی AI |
| `POST /api/crop-simulation/growth/` | `implemented` | `async live AI inference` | route واقعی AI |
| `GET /api/crop-simulation/growth/{task_id}/status/` | `implemented` | `async status` | route واقعی AI |
## مواردی که نباید به‌عنوان route واقعی AI معرفی شوند
| Endpoint | تصمیم |
|---|---|
| `POST /api/farm-alerts/timeline/` | `missing` |
| `GET /api/fertilization/recommend/{task_id}/status/` | `stub/contract-only` |
| `GET /api/irrigation/recommend/{task_id}/status/` | `stub/contract-only` |
| هر route موجود فقط در `Backend/external_api_adapter/json/ai/index.json` و بدون registration واقعی | `stub/contract-only` |
## توضیح مهم درباره mock/spec
فایل `Backend/external_api_adapter/json/ai/index.json` باید به‌عنوان `contract/mock catalog` دیده شود، نه لیست endpointهای تضمین‌شده‌ی production.
اگر endpoint فقط در آن فایل وجود دارد ولی در `Ai/config/urls.py` و routeهای اپ‌ها ثبت نشده، وضعیت آن `stub/contract-only` است.
## Ownership مهم
- plant catalog canonical در Backend شروع می‌شود و AI snapshot/read-model آن را ingest می‌کند.
- `farm_data` در AI facade canonical برای مصرف AI روی farm/sensor/plant assignment است.
- relation قدیمی `SensorData.plants` transitional است و نباید به‌عنوان source-of-truth جدید مستند شود.
## Known Gaps / Follow-up
- schema UI غیرفعال است؛ audit docs منبع فعلی truth هستند.
- بعضی endpointها در backend و AI هر دو وجود دارند اما semantics آن‌ها متفاوت است؛ همیشه live/cached/proxy بودن را جداگانه مستند کنید.
+55
View File
@@ -0,0 +1,55 @@
# AI Apps URL Audit
This document lists the actual AI-service routes registered today and labels their readiness accurately.
## Canonical AI Route Inventory
| App | Method | Route | Status | Notes |
|---|---|---|---:|---|
| `rag` | `POST` | `/api/rag/chat/` | `implemented` | Live AI chat route. |
| `farm_alerts` | `POST` | `/api/farm-alerts/tracker/` | `implemented` | Live AI tracker route. |
| `location_data` | `GET` | `/api/soil-data/` | `implemented` | Live soil-data fetch route. |
| `location_data` | `POST` | `/api/soil-data/` | `implemented` | Live soil-data fetch route. |
| `location_data` | `GET` | `/api/soil-data/tasks/<task_id>/status/` | `implemented` | Live task status route. |
| `location_data` | `POST` | `/api/soil-data/ndvi-health/` | `implemented` | Live AI NDVI route. |
| `soile` | `POST` | `/api/soile/anomaly-detection/` | `implemented` | Live AI route. |
| `soile` | `POST` | `/api/soile/health-summary/` | `implemented` | Live AI route. |
| `soile` | `POST` | `/api/soile/moisture-heatmap/` | `implemented` | Live AI route. |
| `farm_data` | `POST` | `/api/farm-data/` | `implemented` | Upsert route. |
| `farm_data` | `GET` | `/api/farm-data/<farm_uuid>/detail/` | `implemented` | Farm detail route. |
| `farm_data` | `POST` | `/api/farm-data/parameters/` | `implemented` | Sensor parameter create route. |
| `farm_data` | `POST` | `/api/farm-data/plants/sync/` | `implemented` | Internal sync route. |
| `weather` | `POST` | `/api/weather/farm-card/` | `implemented` | Live weather card route. |
| `weather` | `POST` | `/api/weather/water-need-prediction/` | `implemented` | Live water need prediction route. |
| `economy` | `POST` | `/api/economy/overview/` | `implemented` | Live economy route. |
| `plant` | `GET` | `/api/plants/` | `implemented` | Live route. |
| `plant` | `POST` | `/api/plants/` | `implemented` | Live route. |
| `plant` | `GET` | `/api/plants/names/` | `implemented` | Extra route not always reflected in older audits. |
| `plant` | `GET` | `/api/plants/<pk>/` | `implemented` | Live route. |
| `plant` | `POST` | `/api/plants/fetch-info/` | `implemented` | Live route, but operational reliability may still be limited. |
| `pest_disease` | `POST` | `/api/pest-disease/detect/` | `implemented` | Live route. |
| `pest_disease` | `POST` | `/api/pest-disease/risk/` | `implemented` | Live route. |
| `irrigation` | `GET` | `/api/irrigation/` | `implemented` | Live route. |
| `irrigation` | `POST` | `/api/irrigation/` | `implemented` | Live route on AI service. |
| `irrigation` | `GET` | `/api/irrigation/<pk>/` | `implemented` | Live route on AI service. |
| `irrigation` | `PUT` | `/api/irrigation/<pk>/` | `implemented` | Live route on AI service. |
| `irrigation` | `PATCH` | `/api/irrigation/<pk>/` | `implemented` | Live route on AI service. |
| `irrigation` | `DELETE` | `/api/irrigation/<pk>/` | `implemented` | Live route on AI service. |
| `irrigation` | `POST` | `/api/irrigation/recommend/` | `implemented` | Live route. |
| `irrigation` | `POST` | `/api/irrigation/plan-from-text/` | `implemented` | Live route. |
| `irrigation` | `POST` | `/api/irrigation/water-stress/` | `implemented` | Live route. |
| `fertilization` | `POST` | `/api/fertilization/recommend/` | `implemented` | Live route. |
| `fertilization` | `POST` | `/api/fertilization/plan-from-text/` | `implemented` | Live route. |
| `crop_simulation` | `POST` | `/api/crop-simulation/current-farm-chart/` | `implemented` | Live route. |
| `crop_simulation` | `POST` | `/api/crop-simulation/harvest-prediction/` | `implemented` | Live route. |
| `crop_simulation` | `GET` | `/api/crop-simulation/yield-harvest-summary/` | `implemented` | Live route. |
| `crop_simulation` | `POST` | `/api/crop-simulation/yield-prediction/` | `implemented` | Live route. |
| `crop_simulation` | `POST` | `/api/crop-simulation/growth/` | `implemented` | Live route. |
| `crop_simulation` | `GET` | `/api/crop-simulation/growth/<task_id>/status/` | `implemented` | Live route. |
## Important Corrections
- `farm-alerts/timeline` is not an AI route and must not be listed as one.
- `risk-summary` belongs to backend aliasing in `Backend/pest_detection`, not to the AI `pest_disease` app.
- `plant` and `irrigation` have richer real route coverage than older audits claimed.
- `location_data`, `farm_data`, and `crop_simulation` routes are real service routes and should not be described as mock-only.
+39
View File
@@ -0,0 +1,39 @@
FROM docker.iranserver.com/python:3.10
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# Debian/debian mirrors for apt
RUN rm -f /etc/apt/sources.list /etc/apt/sources.list.d/* && \
printf '%s\n' \
'deb https://mirror-linux.runflare.com/debian/ bookworm main contrib non-free non-free-firmware' \
'deb https://mirror-linux.runflare.com/debian/ bookworm-updates main contrib non-free non-free-firmware' \
'deb https://mirror-linux.runflare.com/debian-security/ bookworm-security main contrib non-free non-free-firmware' \
'' \
> /etc/apt/sources.list
# System deps for MySQL client (pkg-config required by mysqlclient to find libs)
RUN apt-get update && apt-get install -y --no-install-recommends \
default-libmysqlclient-dev \
build-essential \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt constraints.txt ./
RUN PIP_CONSTRAINT=/app/constraints.txt \
pip install \
--prefer-binary \
--trusted-host mirror-pypi.runflare.com \
-r requirements.txt
COPY entrypoint.sh /app/entrypoint.sh
COPY . .
EXPOSE 8000
ENTRYPOINT ["sh", "/app/entrypoint.sh"]
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
+39
View File
@@ -0,0 +1,39 @@
FROM mirror-docker.runflare.com/library/python:3.10
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /app
# Debian mirror configuration
RUN rm -f /etc/apt/sources.list /etc/apt/sources.list.d/* && \
printf '%s\n' \
'deb https://mirror-linux.runflare.com/debian/ bookworm main contrib non-free non-free-firmware' \
'deb https://mirror-linux.runflare.com/debian/ bookworm-updates main contrib non-free non-free-firmware' \
'deb https://mirror-linux.runflare.com/debian-security/ bookworm-security main contrib non-free non-free-firmware' \
'' \
> /etc/apt/sources.list
RUN apt-get update && apt-get install -y --no-install-recommends \
default-libmysqlclient-dev \
build-essential \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt constraints.txt /app/
RUN PIP_CONSTRAINT=/app/constraints.txt \
pip install \
--prefer-binary \
--index-url https://mirror-pypi.runflare.com/simple \
--trusted-host mirror-pypi.runflare.com \
-r requirements.txt
COPY entrypoint.sh /app/entrypoint.sh
COPY . .
EXPOSE 8000
ENTRYPOINT ["sh", "/app/entrypoint.sh"]
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
@@ -0,0 +1,688 @@
# راهنمای کامل PCSE در این پروژه
این سند توضیح می‌دهد `PCSE` در این پروژه دقیقا چه نقشی دارد، چگونه داده‌ها را مصرف می‌کند، خروجی آن چگونه ساخته می‌شود، و این خروجی چه اثری روی توصیه‌های آبیاری و کودهی دارد. تمرکز اصلی این راهنما روی اتصال بین این فایل‌ها است:
- `crop_simulation/services.py`
- `crop_simulation/recommendation_optimizer.py`
- `crop_simulation/apps.py`
- `irrigation/apps.py`
- `fertilization/apps.py`
- `rag/services/irrigation.py`
- `rag/services/fertilization.py`
---
## 1) PCSE چیست؟
`PCSE` مخفف `Python Crop Simulation Environment` است. این کتابخانه یک چارچوب شبیه‌سازی زراعی است که می‌تواند با مدل‌هایی مثل `WOFOST` رشد گیاه، توسعه فنولوژیک، تولید زیست‌توده، عملکرد، و پاسخ به آب و نیتروژن را شبیه‌سازی کند.
در این پروژه، PCSE نقش این‌ها را بر عهده دارد:
- تبدیل داده‌های مزرعه، هوا، خاک و برنامه مدیریت به یک اجرای شبیه‌سازی
- برآورد خروجی‌های کلیدی مثل:
- `yield_estimate`
- `biomass`
- `max_lai`
- مقایسه سناریوهای مختلف آبیاری و کودهی
- تولید یک مبنای عددی برای recommendation engine
به زبان ساده:
- `RAG` متن توصیه را خوش‌بیان و کاربرپسند می‌کند
- `PCSE` منطق عددی و شبیه‌سازی سناریویی را تامین می‌کند
---
## 2) معماری کلی PCSE در این پروژه
جریان اصلی به این صورت است:
1. داده مزرعه از دیتابیس خوانده می‌شود
2. داده هوا از forecastها به فرمت قابل فهم برای PCSE تبدیل می‌شود
3. داده خاک و وضعیت سایت ساخته می‌شود
4. پروفایل گیاه و `agromanagement` آماده می‌شود
5. اگر recommendation آبیاری یا کودهی وجود داشته باشد، به `TimedEvents` تزریق می‌شود
6. مدل PCSE اجرا می‌شود
7. خروجی‌های روزانه، خلاصه و نهایی جمع می‌شوند
8. از روی آن‌ها شاخص عملکرد و recommendation سناریویی ساخته می‌شود
9. RAG از این خروجی به‌عنوان `context_text` استفاده می‌کند تا متن نهایی توصیه را بسازد
---
## 3) نقطه ورود اصلی PCSE
### `crop_simulation/apps.py`
در `crop_simulation/apps.py` یک optimizer سراسری lazy-loaded تعریف شده:
- `recommendation_optimizer`
- `get_recommendation_optimizer()`
این optimizer از کلاس `SimulationRecommendationOptimizer` در `crop_simulation/recommendation_optimizer.py` ساخته می‌شود.
بنابراین:
- `rag/services/irrigation.py` از `apps.get_app_config("crop_simulation").get_recommendation_optimizer()` استفاده می‌کند
- `rag/services/fertilization.py` هم همین کار را انجام می‌دهد
یعنی هر دو سرویس recommendation در نهایت به موتور شبیه‌سازی crop_simulation وصل هستند.
---
## 4) نقش `irrigation/apps.py`
فایل `irrigation/apps.py` فقط یک AppConfig ساده نیست؛ در عمل تنظیمات پایه optimizer آبیاری را نگه می‌دارد.
### پارامترهای مهم
#### `simulation_model`
مدل پیش‌فرض:
```python
"Wofost81_NWLP_CWB_CNB"
```
این یعنی recommendationهای آبیاری قرار است روی مدلی ساخته شوند که هم water-limited و هم nutrient-aware است.
#### `validity_days`
```python
3
```
توصیه آبیاری کوتاه‌مدت است و بیشتر به forecastهای نزدیک متکی است.
#### `minimum_event_mm`
```python
4.0
```
هر نوبت آبیاری نباید از این کمتر شود، چون در عمل آبیاری‌های خیلی کوچک یا بی‌اثرند یا اجرای میدانی ضعیفی دارند.
#### `significant_rain_threshold_mm`
```python
4.0
```
اگر بارش موثر به این آستانه برسد، recommendation می‌تواند محافظه‌کارتر شود یا پنجره اعتبار کوتاه‌تر شود.
#### `stage_targets`
برای هر مرحله رشد یک هدف رطوبت خاک تعریف شده:
- `initial`: 65%
- `vegetative`: 70%
- `flowering`: 75%
- `fruiting`: 68%
این‌ها مستقیما بر `moisture_target_percent` در توصیه نهایی اثر می‌گذارند.
#### `strategy_profiles`
سه استراتژی اصلی برای آبیاری تعریف شده:
- `conservative`
- `balanced`
- `protective`
هر استراتژی این مولفه‌ها را دارد:
- `multiplier`
- `frequency_factor`
- `event_count`
این‌ها تعیین می‌کنند:
- جمع کل آب بیشتر یا کمتر شود
- تعداد نوبت‌ها بیشتر یا کمتر شود
- توزیع آب در طول بازه forecast چگونه باشد
---
## 5) نقش `fertilization/apps.py`
این فایل هم مثل نسخه آبیاری، تنظیمات پایه recommendation optimizer کودهی را نگه می‌دارد.
### پارامترهای مهم
#### `simulation_model`
باز هم مدل:
```python
"Wofost81_NWLP_CWB_CNB"
```
یعنی recommendation کودهی بر مبنای مدلی ساخته می‌شود که نیتروژن را در سطح شبیه‌سازی لحاظ می‌کند.
#### `validity_days`
```python
7
```
پنجره اعتبار کودهی بلندتر از آبیاری است، چون تصمیم تغذیه‌ای معمولاً کندتر و با اثر تجمعی‌تر است.
#### `rain_delay_threshold_mm`
```python
3.0
```
اگر بارش موثر نزدیک باشد، برخی روش‌های مصرف یا زمان مصرف می‌توانند نامناسب شوند.
#### `stage_targets`
برای هر مرحله رشد، هدف تغذیه‌ای جداگانه تعریف شده است:
- `n`
- `p`
- `k`
- `formula`
- `application_method`
- `timing`
مثلا در مرحله `flowering`:
- نیاز پتاس بالاتر می‌شود
- فرمول `15-10-30` پیشنهاد می‌شود
- روش مصرف و timing هم متناسب با حساسیت گیاه تعیین می‌شود
#### `strategy_profiles`
سه استراتژی اصلی:
- `maintenance`
- `balanced`
- `corrective`
هر کدام ضریب مصرف و تمرکز متفاوت دارند. این استراتژی‌ها پایه سناریوسازی برای شبیه‌سازی یا heuristic هستند.
---
## 6) PCSE در `crop_simulation/services.py` چگونه کار می‌کند؟
### 6.1 نرمال‌سازی ورودی‌ها
قبل از اجرای مدل، ورودی‌ها یکنواخت می‌شوند:
- `_normalize_weather_records()`
- `_normalize_agromanagement()`
- `_normalize_site_parameters_for_model()`
### 6.2 ساخت payload مزرعه
تابع `build_simulation_payload_from_farm()` اطلاعات زیر را می‌سازد:
- weather
- soil
- site_parameters
- crop_parameters
- agromanagement
منابع آن:
- `SensorData`
- `WeatherForecast`
- پروفایل گیاه
- داده لایه خاک
### 6.3 ساخت داده خاک و سایت
در این مرحله مقادیر مهمی مثل این‌ها ساخته می‌شوند:
- `SMFCF`
- `SMW`
- `RDMSOL`
- `WAV`
- `NAVAILI`
- `P_STATUS`
- `K_STATUS`
- `SOIL_PH`
- `EC`
تعبیر عملی این مقادیر:
- `WAV`: آب در دسترس اولیه
- `NAVAILI`: نیتروژن اولیه در دسترس
- `P_STATUS` و `K_STATUS`: شاخص‌های وضعیت فسفر و پتاسیم
- `SOIL_PH` و `EC`: شرایط شیمیایی که روی کارایی تغذیه و رشد اثر دارند
### 6.4 لود کردن bindingهای PCSE
تابع `_load_pcse_bindings()` این اجزا را از package `pcse` می‌گیرد:
- `ParameterProvider`
- `WeatherDataProvider`
- `WeatherDataContainer`
- `pcse.models`
اگر package نصب نباشد، اجرای سناریوی واقعی PCSE ممکن نیست.
### 6.5 اجرای مدل
کلاس `PcseSimulationManager` قلب اجرای شبیه‌سازی است.
متد `run_simulation()` این کارها را انجام می‌دهد:
1. ساخت `PreparedSimulationInput`
2. normalize کردن weather / soil / crop / site / agromanagement
3. اجرای `_run_with_pcse()`
4. در مدل‌های `Wofost81_NWLP` اعمال adjustment برای `P` و `K`
### 6.6 خروجی‌های اصلی مدل
بعد از اجرا، این سه نوع خروجی جمع می‌شوند:
- `daily_output`
- `summary_output`
- `terminal_output`
و در نهایت metrics اصلی ساخته می‌شود:
- `yield_estimate`
- `biomass`
- `max_lai`
---
## 7) eventهای recommendation چگونه وارد شبیه‌سازی می‌شوند؟
این مهم‌ترین بخش اتصال PCSE به recommendationها است.
### `_parse_recommendation_events()`
این تابع recommendation خام را به event قابل الحاق تبدیل می‌کند.
برای آبیاری:
- `event_signal = "irrigate"`
- کلید مقدار می‌تواند `amount` یا `irrigation_amount` باشد
برای کودهی:
- `event_signal = "apply_n"`
- کلید مقدار می‌تواند `N_amount` یا `amount` باشد
### `_merge_management_recommendations()`
این تابع recommendationها را داخل `TimedEvents` همان campaign اصلی قرار می‌دهد.
پس وقتی شما یک recommendation جدید می‌دهید، در عمل:
- یک برنامه مدیریت جدید ساخته نمی‌شود
- همان `agromanagement` پایه مزرعه گرفته می‌شود
- eventهای جدید به آن تزریق می‌شوند
این طراحی مهم است چون:
- تقویم اصلی کشت حفظ می‌شود
- فقط تصمیم‌های مدیریتی جدید روی سناریو سوار می‌شوند
---
## 8) recommendation optimizer دقیقا چه می‌کند؟
فایل `crop_simulation/recommendation_optimizer.py` لایه تصمیم‌گیری سناریویی است.
کلاس اصلی:
- `SimulationRecommendationOptimizer`
این کلاس دو مسیر دارد:
- مسیر مبتنی بر PCSE
- مسیر heuristic fallback
اگر داده کافی و پروفایل simulation گیاه موجود باشد، اول تلاش می‌کند از PCSE استفاده کند. اگر نشد، به heuristic برمی‌گردد.
---
## 9) تاثیر PCSE روی توصیه آبیاری
### 9.1 ورودی‌های optimizer آبیاری
متد:
- `optimize_irrigation()`
ورودی‌ها:
- `sensor`
- `plant`
- `forecasts`
- `daily_water_needs`
- `growth_stage`
- `irrigation_method`
### 9.2 وقتی مسیر PCSE فعال می‌شود
اگر برای گیاه `simulation profile` معتبر وجود داشته باشد، متد `_optimize_irrigation_with_pcse()` اجرا می‌شود.
در این مسیر:
1. تنظیمات از `irrigation/apps.py` خوانده می‌شود
2. soil/site از سنسور و عمق خاک ساخته می‌شود
3. forecastها به weather record تبدیل می‌شوند
4. از روی `strategy_profiles` چند سناریوی آبیاری ساخته می‌شود
5. برای هر سناریو، eventهای `irrigate` به `agromanagement` تزریق می‌شود
6. هر سناریو با `run_single_simulation()` اجرا می‌شود
7. از روی `yield_estimate` هر سناریو، `score` ساخته می‌شود
8. بهترین سناریو انتخاب می‌شود
### 9.3 PCSE دقیقا چه چیزی را تغییر می‌دهد؟
PCSE باعث می‌شود recommendation آبیاری فقط بر پایه ET یا بارش نباشد. بلکه تاثیر برنامه آبیاری روی شاخص عملکرد گیاه هم دیده شود.
یعنی سیستم فقط نمی‌گوید:
- امروز 8 میلی‌متر آب بده
بلکه عملاً سناریوها را مقایسه می‌کند:
- اگر آب کمتر بدهیم، عملکرد چقدر افت می‌کند؟
- اگر آب را در نوبت‌های بیشتری پخش کنیم، نتیجه بهتر می‌شود؟
- اگر آبیاری حمایتی بدهیم، نسبت آب به عملکرد چطور تغییر می‌کند؟
### 9.4 خروجی نهایی آبیاری
خروجی recommendation آبیاری شامل این فیلدهاست:
- `total_irrigation_mm`
- `amount_per_event_mm`
- `events`
- `event_dates`
- `timing`
- `moisture_target_percent`
- `validity_period`
- `reasoning`
### 9.5 اثر مستقیم `irrigation/apps.py`
مقادیر این فایل مستقیم روی recommendation اثر دارند:
- `stage_targets` هدف رطوبت خاک را تعیین می‌کند
- `strategy_profiles` candidate scenarioها را تعریف می‌کند
- `validity_days` متن و پنجره اعتبار را تعیین می‌کند
- `minimum_event_mm` جلوی recommendationهای غیرعملی را می‌گیرد
- `significant_rain_threshold_mm` روی logic بارش موثر اثر دارد
### 9.6 اگر PCSE در دسترس نباشد
مسیر `_optimize_irrigation_with_heuristic()` استفاده می‌شود.
در این مسیر امتیازدهی بر اساس این‌هاست:
- نیاز آبی forecast
- بارش موثر
- رطوبت فعلی خاک
- دمای بالا
- باد
- بازده روش آبیاری
اما در این حالت شبیه‌سازی واقعی عملکرد انجام نمی‌شود. پس recommendation سبک‌تر و تخمینی‌تر است.
---
## 10) تاثیر PCSE روی توصیه کودهی
### 10.1 ورودی‌های optimizer کودهی
متد:
- `optimize_fertilization()`
ورودی‌ها:
- `sensor`
- `plant`
- `forecasts`
- `growth_stage`
### 10.2 مسیر PCSE برای کودهی
اگر simulation profile و forecast موجود باشد، `_optimize_fertilization_with_pcse()` اجرا می‌شود.
در این مسیر:
1. تنظیمات از `fertilization/apps.py` خوانده می‌شود
2. stage target مرحله رشد تعیین می‌شود
3. برای هر strategy profile یک دوز نیتروژن متفاوت ساخته می‌شود
4. event `apply_n` روی `TimedEvents` قرار می‌گیرد
5. هر سناریوی کودهی با PCSE اجرا می‌شود
6. `yield_estimate` سناریوها مقایسه می‌شود
7. بهترین استراتژی انتخاب می‌شود
### 10.3 PCSE دقیقا چه کمکی می‌کند؟
PCSE باعث می‌شود recommendation کودهی فقط بر اساس کمبود لحظه‌ای عناصر نباشد، بلکه اثر احتمالی سناریوی مصرف روی خروجی گیاه هم دیده شود.
یعنی سیستم فقط نمی‌گوید:
- چون نیتروژن پایین است، فلان مقدار کود بده
بلکه مقایسه می‌کند:
- اگر سناریوی نگهدارنده اجرا شود، عملکرد چقدر می‌شود؟
- اگر سناریوی اصلاحی اجرا شود، gain عملکردی چقدر است؟
- آیا افزایش دوز واقعاً ارزش دارد یا خیر؟
### 10.4 اثر `fertilization/apps.py`
مقادیر این فایل مستقیما بر recommendation اثر دارند:
- `stage_targets` دوز هدف N/P/K را تعیین می‌کند
- `formula` نوع کود پیشنهادی را تعیین می‌کند
- `application_method` روش مصرف را تعیین می‌کند
- `timing` زمان مناسب مصرف را تعیین می‌کند
- `strategy_profiles` سناریوهای رقابتی را می‌سازد
- `rain_delay_threshold_mm` روی ریسک زمان‌بندی مصرف اثر دارد
- `validity_days` پنجره اعتبار را تعیین می‌کند
### 10.5 heuristic fallback در کودهی
اگر PCSE اجرا نشود، `_optimize_fertilization_with_heuristic()` استفاده می‌شود.
این مسیر بر این مبنا تصمیم می‌گیرد:
- نیتروژن فعلی
- فسفر فعلی
- پتاسیم فعلی
- pH خاک
- مرحله رشد
- بارش پیش‌رو
خروجی آن هنوز ساختاریافته و مفید است، اما مثل مسیر PCSE مقایسه عملکرد شبیه‌سازی‌شده ندارد.
---
## 11) نقش P و K در حالی که event کودهی فقط `apply_n` است
در نسخه فعلی شبیه‌سازی، event مستقیم کودهی که وارد `TimedEvents` می‌شود بیشتر روی `N_amount` تمرکز دارد.
اما پروژه برای `P` و `K` هم یک adjustment تکمیلی دارد:
- `_estimate_pk_stress_factor()`
- `_apply_pk_adjustment()`
این بخش بعد از اجرای PCSE روی خروجی اعمال می‌شود.
منطق آن:
- اگر فسفر پایین باشد، `p_factor` کاهش می‌یابد
- اگر پتاسیم پایین باشد، `k_factor` کاهش می‌یابد
- اگر `pH` یا `EC` نامناسب باشد، penalty اعمال می‌شود
- سپس این factor روی:
- `yield_estimate`
- `biomass`
- `max_lai`
اعمال می‌شود
پس حتی اگر event سناریویی مستقیم بیشتر نیتروژنی باشد، وضعیت `P`, `K`, `pH`, `EC` باز هم روی recommendation نهایی اثر می‌گذارد.
---
## 12) RAG چطور از خروجی PCSE استفاده می‌کند؟
### در `rag/services/irrigation.py`
این سرویس:
1. forecastها و داده مزرعه را می‌گیرد
2. نیاز آبی روزانه را از FAO-56 محاسبه می‌کند
3. optimizer شبیه‌سازی را صدا می‌زند
4. اگر `optimized_result` موجود باشد، `context_text` آن را به prompt اضافه می‌کند
5. LLM پاسخ را تولید می‌کند
6. پاسخ با fallback ساختاریافته merge می‌شود
نکته مهم:
- LLM مرجع عددی اصلی نیست
- `optimized_result` مرجع اصلی اعداد است
این موضوع حتی در prompt پیش‌فرض هم صریح آمده است.
### در `rag/services/fertilization.py`
منطق مشابه است:
1. sensor و forecast خوانده می‌شود
2. optimizer کودهی اجرا می‌شود
3. `context_text` به system prompt اضافه می‌شود
4. LLM متن recommendation را می‌سازد
5. خروجی با fallback عددی merge می‌شود
در نتیجه:
- PCSE و optimizer عددها را می‌سازند
- RAG متن را کاربرپسند و اجرایی می‌کند
---
## 13) تفاوت بین simulation engine و recommendation layer
### لایه simulation
در `crop_simulation/services.py`:
- سناریو اجرا می‌شود
- eventها merge می‌شوند
- خروجی‌های عملکردی تولید می‌شوند
### لایه recommendation
در `crop_simulation/recommendation_optimizer.py`:
- چند سناریو candidate ساخته می‌شود
- همه با simulation یا heuristic ارزیابی می‌شوند
- بهترین گزینه انتخاب می‌شود
- `context_text` برای RAG تولید می‌شود
### لایه presentation
در `rag/services/irrigation.py` و `rag/services/fertilization.py`:
- متن نهایی
- هشدارها
- list itemها
- توضیح توسعه‌پذیر
ساخته می‌شود.
---
## 14) سناریوی واقعی آبیاری در این پروژه
یک نمونه ساده:
1. forecast هفت روز آینده دریافت می‌شود
2. نیاز آبی روزانه محاسبه می‌شود
3. optimizer سه سناریوی آبیاری می‌سازد:
- محافظه‌کارانه
- متعادل
- حمایتی
4. هر سناریو به `TimedEvents` تزریق می‌شود
5. PCSE برای هر سناریو اجرا می‌شود
6. عملکرد نسبی هر سناریو اندازه‌گیری می‌شود
7. بهترین سناریو انتخاب می‌شود
8. RAG همان سناریو را به زبان قابل فهم برای کاربر توضیح می‌دهد
---
## 15) سناریوی واقعی کودهی در این پروژه
یک نمونه ساده:
1. مرحله رشد تشخیص داده می‌شود
2. target غذایی همان مرحله از `fertilization/apps.py` خوانده می‌شود
3. چند سناریوی دوز و شدت مصرف ساخته می‌شود
4. برای هر سناریو event `apply_n` ساخته می‌شود
5. PCSE سناریوها را اجرا می‌کند
6. خروجی عملکرد مقایسه می‌شود
7. بهترین برنامه انتخاب می‌شود
8. RAG آن را به صورت JSON ساختاریافته به کاربر برمی‌گرداند
---
## 16) مزیت‌های استفاده از PCSE در توصیه آبیاری و کودهی
- recommendationها فقط rule-based نیستند
- تصمیم‌ها بر پایه مقایسه سناریو هستند
- امکان اتصال داده واقعی مزرعه به مدل رشد وجود دارد
- مرحله رشد، هوا، خاک و مدیریت همزمان دیده می‌شوند
- recommendation خروجی قابل توضیح‌تری برای LLM تولید می‌کند
---
## 17) محدودیت‌های فعلی پیاده‌سازی
این پروژه عملی و مفید است، اما چند محدودیت مهم دارد:
- کیفیت recommendation وابسته به کیفیت `simulation profile` گیاه است
- اگر پروفایل simulation وجود نداشته باشد، سیستم به heuristic fallback می‌رود
- event کودهی در شبیه‌سازی فعلی بیشتر نیتروژن‌محور است
- `P` و `K` به شکل adjustment پس از اجرا اعمال می‌شوند، نه لزوما event-driven کامل
- forecastهای هوا کوتاه‌مدت‌اند؛ پس recommendationها مخصوص تصمیم‌گیری عملیاتی نزدیک هستند
- score برخی سناریوها از `yield_estimate / 100` ساخته می‌شود و هنوز می‌تواند با calibration دقیق‌تر بهبود یابد
---
## 18) جمع‌بندی عملی
اگر بخواهیم نقش PCSE را در یک جمله خلاصه کنیم:
> PCSE در این پروژه موتور سنجش اثر تصمیم‌های آبیاری و کودهی روی عملکرد احتمالی گیاه است.
و اگر بخواهیم نقش `irrigation/apps.py` و `fertilization/apps.py` را هم در یک جمله بگوییم:
> این دو فایل policy و defaultهای تصمیم‌گیری را تعریف می‌کنند، و optimizer با استفاده از همان policyها سناریو می‌سازد و با PCSE ارزیابی می‌کند.
بنابراین خروجی نهایی recommendation حاصل ترکیب سه لایه است:
1. داده واقعی مزرعه و forecast
2. شبیه‌سازی سناریویی با PCSE
3. تولید پاسخ ساختاریافته و قابل فهم با RAG
---
## 19) فایل‌هایی که اگر بخواهید این سیستم را توسعه دهید باید اول ببینید
- `crop_simulation/services.py`
- `crop_simulation/recommendation_optimizer.py`
- `crop_simulation/apps.py`
- `irrigation/apps.py`
- `fertilization/apps.py`
- `rag/services/irrigation.py`
- `rag/services/fertilization.py`
اگر بخواهید behavior سیستم را تغییر دهید:
- برای تغییر policy آبیاری: `irrigation/apps.py`
- برای تغییر policy کودهی: `fertilization/apps.py`
- برای تغییر منطق ارزیابی سناریو: `crop_simulation/recommendation_optimizer.py`
- برای تغییر اجرای واقعی مدل: `crop_simulation/services.py`
- برای تغییر متن و ساختار پاسخ: `rag/services/irrigation.py` و `rag/services/fertilization.py`
+594
View File
@@ -0,0 +1,594 @@
# مستند API سنسورها برای فرانت
این فایل قرارداد پیشنهادی/هدف برای endpointهای سنسوری زیر است و بر اساس نیاز اعلام‌شده تهیه شده است:
- `GET /api/sensor-7-in-1/summary/`
- `GET /api/sensors/comparison-chart/`
- `GET /api/sensors/radar-chart/`
- `GET /api/sensors/values-list/`
- `GET /api/sensor-external-api/logs/`
نکته مهم:
- این سند بر اساس نیاز محصول و قرارداد موردنظر فرانت نوشته شده است.
- در این قرارداد دیگر `physical_device_uuid` از فرانت گرفته نمی‌شود.
- مبنای جست‌وجو فقط `farm_uuid` است.
- backend باید با استفاده از `farm_uuid`، رکورد مزرعه را پیدا کند و اولین سنسور خاک را به‌عنوان سنسور مبنا انتخاب کند.
- اگر `range` ارسال نشود، backend باید بدون خطا مقدار پیش‌فرض `7` روز را در نظر بگیرد.
---
## 1) قواعد عمومی
### آدرس پایه
- پیشوند تمام مسیرها: `/api/`
### فرمت پاسخ
همه endpointها بهتر است envelope استاندارد زیر را برگردانند:
```json
{
"code": 200,
"msg": "success",
"data": {}
}
```
### پارامترهای مشترک
- `farm_uuid``uuid string` — الزامی
- `range``integer` — اختیاری — پیش‌فرض: `7`
### رفتار `range`
- اگر `range` ارسال نشده باشد: backend باید `7` را استفاده کند.
- اگر `range` کمتر از `1` باشد: بهتر است `400` برگردد.
- اگر `range` خیلی بزرگ باشد: پیشنهاد می‌شود backend آن را محدود کند، مثلا حداکثر `90`.
نمونه:
- `/api/sensor-7-in-1/summary/?farm_uuid=11111111-1111-1111-1111-111111111111`
- `/api/sensors/comparison-chart/?farm_uuid=11111111-1111-1111-1111-111111111111&range=30`
### منطق انتخاب سنسور
با دریافت `farm_uuid`:
1. رکورد `farm_data.SensorData` پیدا می‌شود.
2. از `sensor_payload` اولین سنسور خاک انتخاب می‌شود.
3. اگر چند سنسور موجود باشد، اولویت پیشنهادی:
- اولین کلیدی که با `sensor-7` یا `sensor-7-in-1` شروع می‌شود
- اگر نبود، اولین block معتبر از نوع object
4. همان سنسور برای ساخت summary، chart و values استفاده می‌شود.
### خطاهای مشترک
#### 400 — ورودی نامعتبر
```json
{
"code": 400,
"msg": "داده نامعتبر.",
"data": {
"farm_uuid": [
"This field is required."
]
}
}
```
یا:
```json
{
"code": 400,
"msg": "داده نامعتبر.",
"data": {
"range": [
"range باید عددی بزرگ‌تر از صفر باشد."
]
}
}
```
#### 404 — مزرعه یا سنسور پیدا نشد
```json
{
"code": 404,
"msg": "farm یا سنسور خاک یافت نشد.",
"data": null
}
```
#### 200 — بدون داده کافی
اگر مزرعه وجود داشته باشد ولی history کافی برای بازه در دسترس نباشد، پیشنهاد می‌شود endpoint به‌جای خطا، پاسخ موفق با `data` خالی یا حداقلی برگرداند.
---
## 2) GET /api/sensor-7-in-1/summary/
### هدف
نمایش خلاصه سریع آخرین وضعیت سنسور خاک انتخاب‌شده برای یک مزرعه.
### Query Params
- `farm_uuid``uuid string` — الزامی
- `range``integer` — اختیاری — پیش‌فرض `7`
### منطق پاسخ
- آخرین reading سنسور انتخاب‌شده نمایش داده می‌شود.
- اگر داده historical موجود باشد، trend نسبت به بازه `range` محاسبه می‌شود.
- این endpoint مناسب hero cards و summary cards فرانت است.
### پاسخ موفق نمونه
```json
{
"code": 200,
"msg": "success",
"data": {
"farm_uuid": "11111111-1111-1111-1111-111111111111",
"sensor_key": "sensor-7-1",
"range": 7,
"last_updated_at": "2026-04-29T10:20:00Z",
"summary": {
"soil_moisture": {
"label": "رطوبت خاک",
"value": 31.2,
"unit": "%",
"trend": "up",
"change": 2.1,
"change_unit": "%"
},
"soil_temperature": {
"label": "دمای خاک",
"value": 22.8,
"unit": "°C",
"trend": "stable",
"change": 0.3,
"change_unit": "°C"
},
"soil_ph": {
"label": "pH خاک",
"value": 6.9,
"unit": "",
"trend": "down",
"change": -0.1,
"change_unit": ""
},
"electrical_conductivity": {
"label": "هدایت الکتریکی",
"value": 1.4,
"unit": "mS/cm",
"trend": "stable",
"change": 0.0,
"change_unit": "mS/cm"
},
"nitrogen": {
"label": "نیتروژن",
"value": 28.0,
"unit": "mg/kg",
"trend": "up",
"change": 1.8,
"change_unit": "mg/kg"
},
"phosphorus": {
"label": "فسفر",
"value": 14.5,
"unit": "mg/kg",
"trend": "stable",
"change": 0.4,
"change_unit": "mg/kg"
},
"potassium": {
"label": "پتاسیم",
"value": 21.7,
"unit": "mg/kg",
"trend": "down",
"change": -0.9,
"change_unit": "mg/kg"
}
}
}
}
```
### فیلدهای خروجی
- `farm_uuid`: شناسه مزرعه
- `sensor_key`: کلید سنسور انتخاب‌شده از `sensor_payload`
- `range`: بازه واقعی استفاده‌شده
- `last_updated_at`: زمان آخرین reading یا آخرین به‌روزرسانی
- `summary`: آبجکت شامل KPIهای اصلی
### ساختار هر KPI
- `label`: عنوان فارسی
- `value`: آخرین مقدار
- `unit`: واحد
- `trend`: یکی از `up | down | stable | unknown`
- `change`: اختلاف با ابتدای بازه یا میانگین بازه
- `change_unit`: واحد اختلاف
---
## 3) GET /api/sensors/comparison-chart/
### هدف
برگرداندن داده chart مقایسه‌ای برای چند پارامتر سنسور در طول بازه زمانی.
### Query Params
- `farm_uuid``uuid string` — الزامی
- `range``integer` — اختیاری — پیش‌فرض `7`
### منطق پاسخ
- فقط یک سنسور مبنا از روی `farm_uuid` انتخاب می‌شود.
- برای همان سنسور، سری‌های چند متریک در طول بازه برگردانده می‌شود.
- فرانت می‌تواند آن را به line chart یا multi-series chart تبدیل کند.
### پاسخ موفق نمونه
```json
{
"code": 200,
"msg": "success",
"data": {
"farm_uuid": "11111111-1111-1111-1111-111111111111",
"sensor_key": "sensor-7-1",
"range": 7,
"categories": [
"2026-04-23",
"2026-04-24",
"2026-04-25",
"2026-04-26",
"2026-04-27",
"2026-04-28",
"2026-04-29"
],
"series": [
{
"key": "soil_moisture",
"label": "رطوبت خاک",
"unit": "%",
"data": [29.1, 28.7, 30.4, 30.0, 31.1, 31.0, 31.2]
},
{
"key": "soil_temperature",
"label": "دمای خاک",
"unit": "°C",
"data": [21.4, 21.8, 22.0, 22.1, 22.2, 22.6, 22.8]
},
{
"key": "electrical_conductivity",
"label": "هدایت الکتریکی",
"unit": "mS/cm",
"data": [1.2, 1.3, 1.3, 1.4, 1.4, 1.4, 1.4]
}
]
}
}
```
### فیلدهای خروجی
- `categories`: برچسب‌های محور زمان
- `series`: آرایه سری‌ها
### ساختار هر سری
- `key`: کلید متریک
- `label`: نام نمایشی
- `unit`: واحد
- `data`: آرایه مقادیر هم‌طول با `categories`
---
## 4) GET /api/sensors/radar-chart/
### هدف
دادن داده مناسب radar chart برای مقایسه هم‌زمان وضعیت متریک‌های اصلی سنسور.
### Query Params
- `farm_uuid``uuid string` — الزامی
- `range``integer` — اختیاری — پیش‌فرض `7`
### منطق پاسخ
- برای هر متریک، یک مقدار خلاصه از بازه ساخته می‌شود؛ مثلا:
- آخرین مقدار
- میانگین بازه
- یا score نرمال‌شده 0 تا 100
- برای radar chart پیشنهاد می‌شود score نهایی نرمال‌شده برگردانده شود.
### پاسخ موفق نمونه
```json
{
"code": 200,
"msg": "success",
"data": {
"farm_uuid": "11111111-1111-1111-1111-111111111111",
"sensor_key": "sensor-7-1",
"range": 7,
"labels": [
"رطوبت خاک",
"دمای خاک",
"pH خاک",
"هدایت الکتریکی",
"نیتروژن",
"فسفر",
"پتاسیم"
],
"series": [
{
"name": "وضعیت فعلی",
"data": [72, 64, 81, 58, 69, 61, 74]
}
],
"raw_metrics": [
{
"key": "soil_moisture",
"label": "رطوبت خاک",
"value": 31.2,
"unit": "%",
"score": 72
},
{
"key": "soil_temperature",
"label": "دمای خاک",
"value": 22.8,
"unit": "°C",
"score": 64
},
{
"key": "soil_ph",
"label": "pH خاک",
"value": 6.9,
"unit": "",
"score": 81
}
]
}
}
```
### فیلدهای خروجی
- `labels`: برچسب‌های radar
- `series`: داده آماده برای chart
- `raw_metrics`: داده خام برای tooltip و جزئیات بیشتر
---
## 5) GET /api/sensors/values-list/
### هدف
برگرداندن لیست tabular از مقادیر سنسور برای بازه زمانی، مناسب table یا export.
### Query Params
- `farm_uuid``uuid string` — الزامی
- `range``integer` — اختیاری — پیش‌فرض `7`
### پاسخ موفق نمونه
```json
{
"code": 200,
"msg": "success",
"data": {
"farm_uuid": "11111111-1111-1111-1111-111111111111",
"sensor_key": "sensor-7-1",
"range": 7,
"count": 3,
"items": [
{
"recorded_at": "2026-04-29T10:20:00Z",
"soil_moisture": 31.2,
"soil_temperature": 22.8,
"soil_ph": 6.9,
"electrical_conductivity": 1.4,
"nitrogen": 28.0,
"phosphorus": 14.5,
"potassium": 21.7
},
{
"recorded_at": "2026-04-28T10:20:00Z",
"soil_moisture": 31.0,
"soil_temperature": 22.6,
"soil_ph": 7.0,
"electrical_conductivity": 1.4,
"nitrogen": 27.5,
"phosphorus": 14.1,
"potassium": 22.1
},
{
"recorded_at": "2026-04-27T10:20:00Z",
"soil_moisture": 31.1,
"soil_temperature": 22.2,
"soil_ph": 7.0,
"electrical_conductivity": 1.3,
"nitrogen": 27.2,
"phosphorus": 14.0,
"potassium": 22.6
}
]
}
}
```
### فیلدهای خروجی
- `count`: تعداد رکوردها
- `items`: لیست ردیف‌ها
- هر ردیف شامل timestamp و مقادیر متریک‌ها
### رفتار پیشنهادی
- ترتیب رکوردها: جدید به قدیم
- اگر داده تاریخی نداریم ولی آخرین payload فعلی موجود است، حداقل یک item با آخرین وضعیت برگردانده شود
---
## 6) GET /api/sensor-external-api/logs/
### هدف
نمایش لاگ‌های مربوط به سینک یا واکشی داده سنسور از API بیرونی، مناسب صفحه monitoring یا audit.
### Query Params
- `farm_uuid``uuid string` — الزامی
- `range``integer` — اختیاری — پیش‌فرض `7`
### توضیح دامنه
این endpoint برای نمایش لاگ‌های integration است، نه لزوما readingهای سنسور.
اگر backend هنوز لاگ جداگانه برای external sensor sync نداشته باشد، پیشنهاد می‌شود ساختار زیر مبنای پیاده‌سازی قرار بگیرد.
### پاسخ موفق نمونه
```json
{
"code": 200,
"msg": "success",
"data": {
"farm_uuid": "11111111-1111-1111-1111-111111111111",
"range": 7,
"count": 4,
"items": [
{
"id": 104,
"status": "success",
"source": "sensor-external-api",
"sensor_key": "sensor-7-1",
"requested_at": "2026-04-29T10:20:00Z",
"finished_at": "2026-04-29T10:20:01Z",
"duration_ms": 842,
"http_status": 200,
"message": "داده سنسور با موفقیت واکشی شد."
},
{
"id": 103,
"status": "error",
"source": "sensor-external-api",
"sensor_key": "sensor-7-1",
"requested_at": "2026-04-28T10:20:00Z",
"finished_at": "2026-04-28T10:21:00Z",
"duration_ms": 60000,
"http_status": 504,
"message": "Timeout هنگام واکشی داده سنسور."
}
]
}
}
```
### فیلدهای خروجی
- `status`: یکی از `success | error | timeout | partial`
- `source`: نام provider یا سرویس خارجی
- `requested_at`: زمان شروع درخواست
- `finished_at`: زمان پایان
- `duration_ms`: مدت زمان
- `http_status`: وضعیت HTTP سرویس بیرونی
- `message`: پیام خلاصه برای UI
---
## 7) رفتار پیشنهادی در نبود `range`
در همه endpointهای این سند:
- اگر `range` ارسال نشده باشد:
```json
{
"range": 7
}
```
باید به‌صورت implicit استفاده شود و endpoint نباید خطای validation برگرداند.
---
## 8) رفتار پیشنهادی در نبود `physical_device_uuid`
فرانت نباید `physical_device_uuid` ارسال کند.
backend باید:
- فقط `farm_uuid` را بگیرد
- سنسور را از روی `sensor_payload` یا mapping داخلی انتخاب کند
- `sensor_key` نهایی را در پاسخ برگرداند تا فرانت بداند داده از کدام سنسور آمده است
---
## 9) پیشنهاد استاندارد برای متریک‌ها
برای هماهنگی فرانت و بک، بهتر است حداقل این کلیدها در endpointها پشتیبانی شوند:
- `soil_moisture`
- `soil_temperature`
- `soil_ph`
- `electrical_conductivity`
- `nitrogen`
- `phosphorus`
- `potassium`
### واحدهای پیشنهادی
- `soil_moisture``%`
- `soil_temperature``°C`
- `soil_ph` → بدون واحد
- `electrical_conductivity``mS/cm`
- `nitrogen``mg/kg`
- `phosphorus``mg/kg`
- `potassium``mg/kg`
---
## 10) پیشنهاد برای وضعیت‌های فرانت
### loading
- هنگام request، فرانت skeleton یا spinner نشان دهد
### empty
- اگر `items: []` یا `series: []` برگشت:
- پیام مناسب مثل `داده‌ای برای این بازه ثبت نشده است.` نمایش داده شود
### partial
- اگر بعضی متریک‌ها `null` باشند:
- chart فقط seriesهای موجود را نمایش دهد
- در table برای فیلدهای خالی `—` نمایش داده شود
---
## 11) جمع‌بندی قرارداد
برای این 5 endpoint، قرارداد موردنیاز فرانت به‌صورت خلاصه:
- ورودی اصلی فقط `farm_uuid`
- `physical_device_uuid` حذف شود
- `range` اختیاری باشد
- اگر `range` نیامد، مقدار پیش‌فرض `7` در نظر گرفته شود
- backend اولین سنسور خاک مزرعه را انتخاب کند
- `sensor_key` انتخاب‌شده در response برگردانده شود
- responseها envelope استاندارد `code/msg/data` داشته باشند
+41
View File
@@ -0,0 +1,41 @@
from .common import RouteContract
from .crop_simulation_current_farm_chart import CONTRACT as CROP_SIMULATION_CURRENT_FARM_CHART_CONTRACT
from .crop_simulation_growth import CONTRACT as CROP_SIMULATION_GROWTH_CONTRACT
from .crop_simulation_growth_status import CONTRACT as CROP_SIMULATION_GROWTH_STATUS_CONTRACT
from .crop_simulation_harvest_prediction import CONTRACT as CROP_SIMULATION_HARVEST_PREDICTION_CONTRACT
from .crop_simulation_yield_harvest_summary import CONTRACT as CROP_SIMULATION_YIELD_HARVEST_SUMMARY_CONTRACT
from .crop_simulation_yield_prediction import CONTRACT as CROP_SIMULATION_YIELD_PREDICTION_CONTRACT
from .economy_overview import CONTRACT as ECONOMY_OVERVIEW_CONTRACT
from .farm_data_upsert import CONTRACT as FARM_DATA_UPSERT_CONTRACT
from .fertilization_recommend import CONTRACT as FERTILIZATION_RECOMMEND_CONTRACT
from .irrigation_list import CONTRACT as IRRIGATION_LIST_CONTRACT
from .irrigation_recommend import CONTRACT as IRRIGATION_RECOMMEND_CONTRACT
from .rag_chat import CONTRACT as RAG_CHAT_CONTRACT
from .soile_anomaly_detection import CONTRACT as SOILE_ANOMALY_DETECTION_CONTRACT
from .soile_health_summary import CONTRACT as SOILE_HEALTH_SUMMARY_CONTRACT
from .soile_moisture_heatmap import CONTRACT as SOILE_MOISTURE_HEATMAP_CONTRACT
from .weather_water_need_prediction import CONTRACT as WEATHER_WATER_NEED_PREDICTION_CONTRACT
ROUTE_CONTRACTS: dict[str, RouteContract] = {
contract.path: contract
for contract in [
RAG_CHAT_CONTRACT,
SOILE_MOISTURE_HEATMAP_CONTRACT,
SOILE_HEALTH_SUMMARY_CONTRACT,
SOILE_ANOMALY_DETECTION_CONTRACT,
FARM_DATA_UPSERT_CONTRACT,
WEATHER_WATER_NEED_PREDICTION_CONTRACT,
ECONOMY_OVERVIEW_CONTRACT,
IRRIGATION_LIST_CONTRACT,
IRRIGATION_RECOMMEND_CONTRACT,
FERTILIZATION_RECOMMEND_CONTRACT,
CROP_SIMULATION_GROWTH_CONTRACT,
CROP_SIMULATION_GROWTH_STATUS_CONTRACT,
CROP_SIMULATION_CURRENT_FARM_CHART_CONTRACT,
CROP_SIMULATION_HARVEST_PREDICTION_CONTRACT,
CROP_SIMULATION_YIELD_HARVEST_SUMMARY_CONTRACT,
CROP_SIMULATION_YIELD_PREDICTION_CONTRACT,
]
}
__all__ = ['ROUTE_CONTRACTS', 'RouteContract']
+32
View File
@@ -0,0 +1,32 @@
from __future__ import annotations
from typing import Any, Generic, TypeAlias, TypeVar
from pydantic import BaseModel, ConfigDict
JsonValue: TypeAlias = Any
JsonObject: TypeAlias = dict[str, Any]
JsonList: TypeAlias = list[Any]
T = TypeVar('T')
class SchemaModel(BaseModel):
model_config = ConfigDict(extra='allow', populate_by_name=True)
class ApiEnvelope(SchemaModel, Generic[T]):
code: int
msg: str
data: T
class RouteContract(SchemaModel):
method: str
path: str
request_model: str
response_model: str
class EmptyRequest(SchemaModel):
pass
@@ -0,0 +1,42 @@
from __future__ import annotations
from uuid import UUID
from pydantic import Field
from .common import ApiEnvelope, JsonObject, JsonValue, RouteContract, SchemaModel
HTTP_METHOD = 'POST'
ROUTE_PATH = '/api/crop-simulation/current-farm-chart/'
class CropSimulationCurrentFarmChartRequest(SchemaModel):
farm_uuid: UUID
plant_name: str | None = None
class CropSimulationCurrentFarmChartResponseData(SchemaModel):
farm_uuid: str | None = None
plant_name: str | None = None
engine: str | None = None
model_name: str | None = None
scenario_id: int | None = None
simulation_warning: str | None = None
categories: list[str] = Field(default_factory=list)
series: JsonValue | None = None
summary: JsonObject = Field(default_factory=dict)
current_state: JsonObject = Field(default_factory=dict)
metrics: JsonObject = Field(default_factory=dict)
daily_output: JsonObject = Field(default_factory=dict)
class CropSimulationCurrentFarmChartResponse(ApiEnvelope[CropSimulationCurrentFarmChartResponseData]):
pass
CONTRACT = RouteContract(
method=HTTP_METHOD,
path=ROUTE_PATH,
request_model=CropSimulationCurrentFarmChartRequest.__name__,
response_model=CropSimulationCurrentFarmChartResponse.__name__,
)
@@ -0,0 +1,46 @@
from __future__ import annotations
from uuid import UUID
from pydantic import Field, model_validator
from .common import ApiEnvelope, JsonObject, JsonValue, RouteContract, SchemaModel
HTTP_METHOD = 'POST'
ROUTE_PATH = '/api/crop-simulation/growth/'
class CropSimulationGrowthRequest(SchemaModel):
plant_name: str
dynamic_parameters: list[str] = Field(min_length=1)
farm_uuid: UUID | None = None
weather: JsonValue | None = None
soil_parameters: JsonObject | None = None
site_parameters: JsonObject | None = None
crop_parameters: JsonObject | None = None
agromanagement: JsonObject | None = None
page_size: int | None = Field(default=None, ge=1, le=50)
@model_validator(mode='after')
def validate_farm_or_weather(self) -> 'CropSimulationGrowthRequest':
if self.farm_uuid is None and self.weather is None:
raise ValueError('Either farm_uuid or weather must be provided.')
return self
class CropSimulationGrowthResponseData(SchemaModel):
task_id: str
status_url: str
plant_name: str
class CropSimulationGrowthResponse(ApiEnvelope[CropSimulationGrowthResponseData]):
pass
CONTRACT = RouteContract(
method=HTTP_METHOD,
path=ROUTE_PATH,
request_model=CropSimulationGrowthRequest.__name__,
response_model=CropSimulationGrowthResponse.__name__,
)
@@ -0,0 +1,59 @@
from __future__ import annotations
from pydantic import Field
from .common import ApiEnvelope, JsonObject, JsonValue, JsonList, RouteContract, SchemaModel
HTTP_METHOD = 'GET'
ROUTE_PATH = '/api/crop-simulation/growth/<task_id>/status/'
class CropSimulationGrowthStatusRequest(SchemaModel):
task_id: str
page: int | None = Field(default=None, ge=1)
page_size: int | None = Field(default=None, ge=1)
class CropSimulationPagination(SchemaModel):
page: int
page_size: int
total_items: int
total_pages: int
has_next: bool
has_previous: bool
class CropSimulationGrowthResult(SchemaModel):
plant_name: str | None = None
dynamic_parameters: list[str] = Field(default_factory=list)
engine: str | None = None
model_name: str | None = None
scenario_id: int | None = None
simulation_warning: str | None = None
summary_metrics: JsonObject = Field(default_factory=dict)
stage_timeline: JsonList = Field(default_factory=list)
stages_page: JsonList = Field(default_factory=list)
pagination: CropSimulationPagination | None = None
daily_records_count: int | None = None
default_page_size: int | None = None
class CropSimulationGrowthStatusResponseData(SchemaModel):
task_id: str
status: str
message: str | None = None
progress: JsonObject = Field(default_factory=dict)
result: CropSimulationGrowthResult | None = None
error: str | None = None
class CropSimulationGrowthStatusResponse(ApiEnvelope[CropSimulationGrowthStatusResponseData]):
pass
CONTRACT = RouteContract(
method=HTTP_METHOD,
path=ROUTE_PATH,
request_model=CropSimulationGrowthStatusRequest.__name__,
response_model=CropSimulationGrowthStatusResponse.__name__,
)
@@ -0,0 +1,37 @@
from __future__ import annotations
from uuid import UUID
from pydantic import Field
from .common import ApiEnvelope, JsonObject, RouteContract, SchemaModel
HTTP_METHOD = 'POST'
ROUTE_PATH = '/api/crop-simulation/harvest-prediction/'
class CropSimulationHarvestPredictionRequest(SchemaModel):
farm_uuid: UUID
plant_name: str | None = None
class CropSimulationHarvestPredictionResponseData(SchemaModel):
date: str
dateFormatted: str
daysUntil: int
description: str | None = None
optimalWindowStart: str | None = None
optimalWindowEnd: str | None = None
gddDetails: JsonObject = Field(default_factory=dict)
class CropSimulationHarvestPredictionResponse(ApiEnvelope[CropSimulationHarvestPredictionResponseData]):
pass
CONTRACT = RouteContract(
method=HTTP_METHOD,
path=ROUTE_PATH,
request_model=CropSimulationHarvestPredictionRequest.__name__,
response_model=CropSimulationHarvestPredictionResponse.__name__,
)
@@ -0,0 +1,40 @@
from __future__ import annotations
from uuid import UUID
from pydantic import Field
from .common import ApiEnvelope, JsonObject, RouteContract, SchemaModel
HTTP_METHOD = 'GET'
ROUTE_PATH = '/api/crop-simulation/yield-harvest-summary/'
class CropSimulationYieldHarvestSummaryRequest(SchemaModel):
farm_uuid: UUID
season_year: int | None = None
crop_name: str | None = None
include_narrative: bool | None = None
class CropSimulationYieldHarvestSummaryResponseData(SchemaModel):
farm_uuid: str
season_highlights_card: JsonObject = Field(default_factory=dict)
yield_prediction: JsonObject = Field(default_factory=dict)
harvest_prediction_card: JsonObject = Field(default_factory=dict)
harvest_readiness_zones: JsonObject = Field(default_factory=dict)
yield_quality_bands: JsonObject = Field(default_factory=dict)
harvest_operations_card: JsonObject = Field(default_factory=dict)
yield_prediction_chart: JsonObject = Field(default_factory=dict)
class CropSimulationYieldHarvestSummaryResponse(ApiEnvelope[CropSimulationYieldHarvestSummaryResponseData]):
pass
CONTRACT = RouteContract(
method=HTTP_METHOD,
path=ROUTE_PATH,
request_model=CropSimulationYieldHarvestSummaryRequest.__name__,
response_model=CropSimulationYieldHarvestSummaryResponse.__name__,
)
@@ -0,0 +1,41 @@
from __future__ import annotations
from uuid import UUID
from pydantic import Field
from .common import ApiEnvelope, JsonObject, RouteContract, SchemaModel
HTTP_METHOD = 'POST'
ROUTE_PATH = '/api/crop-simulation/yield-prediction/'
class CropSimulationYieldPredictionRequest(SchemaModel):
farm_uuid: UUID
plant_name: str | None = None
class CropSimulationYieldPredictionResponseData(SchemaModel):
farm_uuid: str
plant_name: str | None = None
predictedYieldTons: float | None = None
predictedYieldRaw: float | None = None
unit: str | None = None
sourceUnit: str | None = None
simulationEngine: str | None = None
simulationModel: str | None = None
scenarioId: int | None = None
simulationWarning: str | None = None
supportingMetrics: JsonObject = Field(default_factory=dict)
class CropSimulationYieldPredictionResponse(ApiEnvelope[CropSimulationYieldPredictionResponseData]):
pass
CONTRACT = RouteContract(
method=HTTP_METHOD,
path=ROUTE_PATH,
request_model=CropSimulationYieldPredictionRequest.__name__,
response_model=CropSimulationYieldPredictionResponse.__name__,
)
+47
View File
@@ -0,0 +1,47 @@
from __future__ import annotations
from uuid import UUID
from pydantic import Field
from .common import ApiEnvelope, RouteContract, SchemaModel
HTTP_METHOD = 'POST'
ROUTE_PATH = '/api/economy/overview/'
class EconomyOverviewRequest(SchemaModel):
farm_uuid: UUID
class EconomyDataItem(SchemaModel):
title: str
value: str
subtitle: str | None = None
avatarIcon: str | None = None
avatarColor: str | None = None
class ChartSeriesItem(SchemaModel):
name: str
data: list[float] = Field(default_factory=list)
class EconomyOverviewResponseData(SchemaModel):
farm_uuid: str
source: str | None = None
economicData: list[EconomyDataItem] = Field(default_factory=list)
chartSeries: list[ChartSeriesItem] = Field(default_factory=list)
chartCategories: list[str] = Field(default_factory=list)
class EconomyOverviewResponse(ApiEnvelope[EconomyOverviewResponseData]):
pass
CONTRACT = RouteContract(
method=HTTP_METHOD,
path=ROUTE_PATH,
request_model=EconomyOverviewRequest.__name__,
response_model=EconomyOverviewResponse.__name__,
)
+53
View File
@@ -0,0 +1,53 @@
from __future__ import annotations
from uuid import UUID
from pydantic import Field, model_validator
from .common import ApiEnvelope, JsonObject, RouteContract, SchemaModel
HTTP_METHOD = 'POST'
ROUTE_PATH = '/api/farm-data/'
class FarmBoundaryCorner(SchemaModel):
lat: float
lon: float
class FarmDataUpsertRequest(SchemaModel):
farm_uuid: UUID
farm_boundary: JsonObject
sensor_key: str | None = 'sensor-7-1'
sensor_payload: JsonObject | None = None
plant_ids: list[int] = Field(default_factory=list)
irrigation_method_id: int | None = None
@model_validator(mode='after')
def validate_payload_sources(self) -> 'FarmDataUpsertRequest':
if not self.sensor_payload and not self.plant_ids and self.irrigation_method_id is None:
raise ValueError('At least one of sensor_payload, plant_ids or irrigation_method_id must be provided.')
return self
class FarmDataUpsertResponseData(SchemaModel):
farm_uuid: UUID
center_location_id: int | None = None
weather_forecast_id: int | None = None
sensor_payload: JsonObject = Field(default_factory=dict)
plant_ids: list[int] = Field(default_factory=list)
irrigation_method_id: int | None = None
created_at: str | None = None
updated_at: str | None = None
class FarmDataUpsertResponse(ApiEnvelope[FarmDataUpsertResponseData]):
pass
CONTRACT = RouteContract(
method=HTTP_METHOD,
path=ROUTE_PATH,
request_model=FarmDataUpsertRequest.__name__,
response_model=FarmDataUpsertResponse.__name__,
)
@@ -0,0 +1,125 @@
from __future__ import annotations
from typing import Literal
from uuid import UUID
from pydantic import Field
from .common import ApiEnvelope, RouteContract, SchemaModel
HTTP_METHOD = 'POST'
ROUTE_PATH = '/api/fertilization/recommend/'
class FertilizationRecommendRequest(SchemaModel):
farm_uuid: UUID
sensor_uuid: UUID | None = None
crop_id: str | None = None
plant_name: str | None = None
growth_stage: str | None = None
query: str | None = None
class NpkRatio(SchemaModel):
n: float
p: float
k: float
label: str
class ApplicationMethod(SchemaModel):
id: str
label: str
class ApplicationInterval(SchemaModel):
value: int
unit: Literal['day', 'week']
label: str
class Dosage(SchemaModel):
base_amount_per_hectare: float
base_amount_per_square_meter: float
unit: Literal['kg', 'gram', 'liter', 'milliliter']
label: str
calculation_basis: str
class PrimaryRecommendation(SchemaModel):
fertilizer_code: str
fertilizer_name: str
display_title: str
fertilizer_type: str
npk_ratio: NpkRatio
application_method: ApplicationMethod
application_interval: ApplicationInterval
dosage: Dosage
reasoning: str
summary: str
class NutrientItem(SchemaModel):
key: str
name: str
value: float
unit: Literal['percent']
description: str | None = None
class NutrientAnalysis(SchemaModel):
macro: list[NutrientItem] = Field(default_factory=list)
micro: list[NutrientItem] = Field(default_factory=list)
class ApplicationGuideStep(SchemaModel):
step_number: int
title: str
description: str
class ApplicationGuide(SchemaModel):
safety_warning: str
steps: list[ApplicationGuideStep] = Field(default_factory=list)
class AlternativeRecommendation(SchemaModel):
fertilizer_code: str
fertilizer_name: str
fertilizer_type: str
usage_method: str
description: str
class FertilizationSection(SchemaModel):
type: Literal['recommendation', 'list', 'warning', 'info']
title: str
icon: str | None = None
content: str | None = None
items: list[str] = Field(default_factory=list)
fertilizerType: str | None = None
amount: str | None = None
applicationMethod: str | None = None
timing: str | None = None
validityPeriod: str | None = None
expandableExplanation: str | None = None
class FertilizationRecommendResponseData(SchemaModel):
primary_recommendation: PrimaryRecommendation
nutrient_analysis: NutrientAnalysis
application_guide: ApplicationGuide
alternative_recommendations: list[AlternativeRecommendation] = Field(default_factory=list)
sections: list[FertilizationSection] = Field(default_factory=list)
class FertilizationRecommendResponse(ApiEnvelope[FertilizationRecommendResponseData]):
pass
CONTRACT = RouteContract(
method=HTTP_METHOD,
path=ROUTE_PATH,
request_model=FertilizationRecommendRequest.__name__,
response_model=FertilizationRecommendResponse.__name__,
)
+39
View File
@@ -0,0 +1,39 @@
from __future__ import annotations
from pydantic import RootModel
from .common import EmptyRequest, RouteContract, SchemaModel
HTTP_METHOD = 'GET'
ROUTE_PATH = '/api/irrigation/'
class IrrigationListRequest(EmptyRequest):
pass
class IrrigationMethodSchema(SchemaModel):
id: int
name: str
category: str | None = None
description: str | None = None
water_efficiency_percent: float | None = None
water_pressure_required: str | None = None
flow_rate: str | None = None
coverage_area: str | None = None
soil_type: str | None = None
climate_suitability: str | None = None
created_at: str | None = None
updated_at: str | None = None
class IrrigationListResponse(RootModel[list[IrrigationMethodSchema]]):
pass
CONTRACT = RouteContract(
method=HTTP_METHOD,
path=ROUTE_PATH,
request_model=IrrigationListRequest.__name__,
response_model=IrrigationListResponse.__name__,
)
@@ -0,0 +1,49 @@
from __future__ import annotations
from typing import Literal
from uuid import UUID
from pydantic import Field
from .common import ApiEnvelope, RouteContract, SchemaModel
HTTP_METHOD = 'POST'
ROUTE_PATH = '/api/irrigation/recommend/'
class IrrigationRecommendRequest(SchemaModel):
farm_uuid: UUID
sensor_uuid: UUID | None = None
plant_name: str | None = None
growth_stage: str | None = None
irrigation_method_name: str | None = None
query: str | None = None
class IrrigationSection(SchemaModel):
type: Literal['recommendation', 'list', 'warning', 'info']
title: str
icon: str | None = None
content: str | None = None
items: list[str] = Field(default_factory=list)
frequency: str | None = None
amount: str | None = None
timing: str | None = None
validityPeriod: str | None = None
expandableExplanation: str | None = None
class IrrigationRecommendResponseData(SchemaModel):
sections: list[IrrigationSection] = Field(default_factory=list)
class IrrigationRecommendResponse(ApiEnvelope[IrrigationRecommendResponseData]):
pass
CONTRACT = RouteContract(
method=HTTP_METHOD,
path=ROUTE_PATH,
request_model=IrrigationRecommendRequest.__name__,
response_model=IrrigationRecommendResponse.__name__,
)
+50
View File
@@ -0,0 +1,50 @@
from __future__ import annotations
from typing import Literal
from uuid import UUID
from pydantic import Field
from .common import ApiEnvelope, JsonObject, JsonValue, RouteContract, SchemaModel
HTTP_METHOD = 'POST'
ROUTE_PATH = '/api/rag/chat/'
class RagChatRequest(SchemaModel):
farm_uuid: UUID
query: str | None = None
message: str | None = None
history: list[JsonObject] | str | None = None
image_urls: list[str] = Field(default_factory=list)
image: str | None = None
images: list[str] = Field(default_factory=list)
class RagChatSection(SchemaModel):
type: Literal['recommendation', 'list', 'warning', 'info', 'summary']
title: str
icon: str | None = None
content: str | None = None
items: list[str] = Field(default_factory=list)
primaryAction: str | None = None
timing: str | None = None
validityPeriod: str | None = None
expandableExplanation: str | None = None
metadata: JsonValue | None = None
class RagChatResponseData(SchemaModel):
sections: list[RagChatSection] = Field(default_factory=list)
class RagChatResponse(ApiEnvelope[RagChatResponseData]):
pass
CONTRACT = RouteContract(
method=HTTP_METHOD,
path=ROUTE_PATH,
request_model=RagChatRequest.__name__,
response_model=RagChatResponse.__name__,
)
@@ -0,0 +1,42 @@
from __future__ import annotations
from typing import Literal
from uuid import UUID
from pydantic import Field
from .common import ApiEnvelope, JsonObject, JsonList, RouteContract, SchemaModel
HTTP_METHOD = 'POST'
ROUTE_PATH = '/api/soile/anomaly-detection/'
class SoileAnomalyDetectionRequest(SchemaModel):
farm_uuid: UUID
class SoileAnomalyDetectionResponseData(SchemaModel):
farm_uuid: str
summary: str
explanation: str | None = None
likely_cause: str | None = None
recommended_action: str | None = None
monitoring_priority: Literal['low', 'medium', 'high', 'urgent'] | str
confidence: float | None = None
generated_at: str | None = None
anomalies: JsonList = Field(default_factory=list)
interpretation: JsonObject = Field(default_factory=dict)
knowledge_base: str | None = None
raw_response: str | None = None
class SoileAnomalyDetectionResponse(ApiEnvelope[SoileAnomalyDetectionResponseData]):
pass
CONTRACT = RouteContract(
method=HTTP_METHOD,
path=ROUTE_PATH,
request_model=SoileAnomalyDetectionRequest.__name__,
response_model=SoileAnomalyDetectionResponse.__name__,
)
@@ -0,0 +1,37 @@
from __future__ import annotations
from uuid import UUID
from pydantic import Field
from .common import ApiEnvelope, JsonObject, RouteContract, SchemaModel
HTTP_METHOD = 'POST'
ROUTE_PATH = '/api/soile/health-summary/'
class SoileHealthSummaryRequest(SchemaModel):
farm_uuid: UUID
class SoileHealthSummaryResponseData(SchemaModel):
farm_uuid: str
healthScore: int | float
profileSource: str | None = None
healthScoreDetails: JsonObject = Field(default_factory=dict)
healthLanguage: JsonObject = Field(default_factory=dict)
avgSoilMoisture: int | float | None = None
avgSoilMoistureRaw: float | None = None
avgSoilMoistureStatus: str | None = None
class SoileHealthSummaryResponse(ApiEnvelope[SoileHealthSummaryResponseData]):
pass
CONTRACT = RouteContract(
method=HTTP_METHOD,
path=ROUTE_PATH,
request_model=SoileHealthSummaryRequest.__name__,
response_model=SoileHealthSummaryResponse.__name__,
)
@@ -0,0 +1,41 @@
from __future__ import annotations
from uuid import UUID
from pydantic import Field
from .common import ApiEnvelope, JsonObject, JsonList, RouteContract, SchemaModel
HTTP_METHOD = 'POST'
ROUTE_PATH = '/api/soile/moisture-heatmap/'
class SoileMoistureHeatmapRequest(SchemaModel):
farm_uuid: UUID
class SoileMoistureHeatmapResponseData(SchemaModel):
farm_uuid: str
location: JsonObject = Field(default_factory=dict)
current_sensor: JsonObject = Field(default_factory=dict)
soil_profile: JsonList = Field(default_factory=list)
timestamp: str | None = None
grid_resolution: JsonObject = Field(default_factory=dict)
grid_cells: JsonList = Field(default_factory=list)
sensor_points: JsonList = Field(default_factory=list)
quality_legend: JsonObject = Field(default_factory=dict)
depth_layers: JsonList = Field(default_factory=list)
model_metadata: JsonObject = Field(default_factory=dict)
summary: JsonObject = Field(default_factory=dict)
class SoileMoistureHeatmapResponse(ApiEnvelope[SoileMoistureHeatmapResponseData]):
pass
CONTRACT = RouteContract(
method=HTTP_METHOD,
path=ROUTE_PATH,
request_model=SoileMoistureHeatmapRequest.__name__,
response_model=SoileMoistureHeatmapResponse.__name__,
)
@@ -0,0 +1,46 @@
from __future__ import annotations
from uuid import UUID
from pydantic import Field
from .common import ApiEnvelope, JsonList, RouteContract, SchemaModel
HTTP_METHOD = 'POST'
ROUTE_PATH = '/api/weather/water-need-prediction/'
class WeatherWaterNeedPredictionRequest(SchemaModel):
farm_uuid: UUID
class WaterNeedInsight(SchemaModel):
summary: str | None = None
irrigation_outlook: str | None = None
recommended_action: str | None = None
risk_note: str | None = None
confidence: float | None = None
class WeatherWaterNeedPredictionResponseData(SchemaModel):
farm_uuid: str
totalNext7Days: float | None = None
unit: str | None = None
categories: list[str] = Field(default_factory=list)
series: JsonList = Field(default_factory=list)
dailyBreakdown: JsonList = Field(default_factory=list)
insight: WaterNeedInsight = Field(default_factory=WaterNeedInsight)
knowledge_base: str | None = None
raw_response: str | None = None
class WeatherWaterNeedPredictionResponse(ApiEnvelope[WeatherWaterNeedPredictionResponseData]):
pass
CONTRACT = RouteContract(
method=HTTP_METHOD,
path=ROUTE_PATH,
request_model=WeatherWaterNeedPredictionRequest.__name__,
response_model=WeatherWaterNeedPredictionResponse.__name__,
)
+18
View File
@@ -0,0 +1,18 @@
try:
import pymysql
except ImportError: # pragma: no cover - optional fallback when mysqlclient is unavailable
pymysql = None
else: # pragma: no cover - import side effect
# Django 5's MySQL backend checks the mysqlclient version string during import.
# PyMySQL exposes a legacy compatibility version, so override it before installing
# the MySQLdb shim.
pymysql.version_info = (2, 2, 1, "final", 0)
pymysql.__version__ = "2.2.1"
pymysql.install_as_MySQLdb()
try:
from .celery import app as celery_app
except ImportError: # pragma: no cover - fallback for test environments
celery_app = None
__all__ = ("celery_app",)
+7
View File
@@ -0,0 +1,7 @@
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_asgi_application()
+56
View File
@@ -0,0 +1,56 @@
import os
from types import SimpleNamespace
from uuid import uuid4
try:
from celery import Celery
except ImportError: # pragma: no cover - test/dev fallback when celery is absent
Celery = None
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
if Celery is not None:
app = Celery("config")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()
else:
class _FallbackCeleryApp:
def config_from_object(self, *_args, **_kwargs):
return None
def autodiscover_tasks(self, *_args, **_kwargs):
return None
def task(self, *decorator_args, **decorator_kwargs):
bind = decorator_kwargs.get("bind", False)
def decorator(func):
def delay(*args, **kwargs):
task_id = f"missing-celery-{uuid4()}"
return SimpleNamespace(
id=task_id,
status="FAILURE",
result={"error": "Celery is not installed."},
)
if bind:
def wrapped(*args, **kwargs):
dummy_self = SimpleNamespace(
request=SimpleNamespace(id=f"missing-celery-{uuid4()}"),
update_state=lambda **_kw: None,
)
return func(dummy_self, *args, **kwargs)
wrapped.delay = delay
wrapped.__name__ = func.__name__
wrapped.__doc__ = func.__doc__
return wrapped
func.delay = delay
return func
if decorator_args and callable(decorator_args[0]) and len(decorator_args) == 1:
return decorator(decorator_args[0])
return decorator
app = _FallbackCeleryApp()
+39
View File
@@ -0,0 +1,39 @@
from __future__ import annotations
from datetime import datetime
from typing import Any
def _isoformat(value: Any) -> Any:
if isinstance(value, datetime):
return value.isoformat()
return value
def build_integration_meta(
*,
flow_type: str,
source_type: str,
source_service: str,
ownership: str,
live: bool,
cached: bool,
generated_at: Any = None,
snapshot_at: Any = None,
notes: list[str] | None = None,
) -> dict[str, Any]:
meta = {
"flow_type": flow_type,
"source_type": source_type,
"source_service": source_service,
"ownership": ownership,
"live": live,
"cached": cached,
}
if generated_at is not None:
meta["generated_at"] = _isoformat(generated_at)
if snapshot_at is not None:
meta["snapshot_at"] = _isoformat(snapshot_at)
if notes:
meta["notes"] = notes
return meta
@@ -0,0 +1,3 @@
# پایگاه دانش CropLogic
فایل‌های `.txt` و `.md` این پوشه به‌صورت خودکار embed و به Qdrant اضافه می‌شوند.
@@ -0,0 +1,3 @@
# پایگاه دانش CropLogic
فایل‌های `.txt` و `.md` این پوشه به‌صورت خودکار embed و به Qdrant اضافه می‌شوند.
@@ -0,0 +1,19 @@
# دانش پایه خاک برای کشاورزی
## انواع خاک
خاک‌ها بر اساس بافت (نسبت رس، سیلت و شن) دسته‌بندی می‌شوند. خاک رسی زهکشی ضعیف‌تری دارد و خاک شنی زهکشی سریع. خاک لومی ترکیبی متعادل از هر سه است و برای اغلب گیاهان مناسب است.
## pH خاک
مقیاس pH از ۰ تا ۱۴ است؛ مقدار ۷ خنثی است. خاک‌های اسیدی (زیر ۷) و قلیایی (بالای ۷) بر جذب عناصر غذایی تأثیر می‌گذارند. بیشتر گیاهان زراعی pH حدود ۶ تا ۷.۵ را ترجیح می‌دهند.
## رطوبت خاک
رطوبت خاک بر رشد ریشه و جذب آب و مواد غذایی تأثیر مستقیم دارد. رطوبت بیش از حد باعث خفگی ریشه و کمبود اکسیژن می‌شود؛ رطوبت کم باعث تنش آبی و کاهش عملکرد می‌شود.
## NPK و عناصر غذایی
نیتروژن (N) برای رشد سبزینه و برگ‌ها ضروری است. فسفر (P) برای ریشه‌زایی و گلدهی مهم است. پتاسیم (K) مقاومت به خشکی و بیماری را افزایش می‌دهد. مقادیر این عناصر در خاک با آزمون خاک قابل اندازه‌گیری است.
## هدایت الکتریکی (EC)
EC نشان‌دهنده شوری خاک است. EC بالا یعنی نمک زیاد و می‌تواند به ریشه گیاه آسیب برساند. واحد آن معمولاً dS/m یا mS/cm است.
## عمق خاک
داده‌های خاک معمولاً در اعماق ۰–۵، ۵–۱۵ و ۱۵–۳۰ سانتی‌متر اندازه‌گیری می‌شوند. لایه سطحی برای جوانه‌زنی و ریشه‌های سطحی مهم است؛ لایه‌های عمیق‌تر برای گیاهان ریشه‌عمیق اهمیت دارند.
@@ -0,0 +1,14 @@
در این پایگاه دانش، هشدارهای مزرعه باید به سه سطح استاندارد تقسیم شوند:
- danger: خطر فوری که به اقدام سریع نیاز دارد.
- warning: هشدار مهم که باید در کوتاه مدت پیگیری شود.
- info: اطلاع رسانی برای پایش، ثبت، یا اقدام کم ریسک.
قاعده های کلی:
1. اگر تنش می تواند باعث آسیب سریع به گیاه، ریشه، یا عملکرد شود، سطح danger مناسب است.
2. اگر تنش هنوز بحرانی نیست ولی روند آن نگران کننده است، سطح warning مناسب است.
3. اگر فقط برای پایش یا آگاهی اپراتور مفید است، سطح info مناسب است.
4. پیام ها باید کوتاه، اجرایی، و بدون اغراق باشند.
5. اگر داده کافی نیست، باید عدم قطعیت به صراحت بیان شود.
6. در متن نهایی فقط از داده های ساختاریافته مزرعه و هشدارهای محاسبه شده استفاده شود.
7. زمان، شدت، و اقدام پیشنهادی باید با وضعیت واقعی مزرعه همخوان باشد.
8. برای timeline باید ترتیب زمانی رویدادها حفظ شود و هر رویداد توضیح دهد چرا برای مزرعه مهم است.
@@ -0,0 +1,142 @@
بخش دوم: راهنمای کوددهی گوجه‌فرنگی
گوجه‌فرنگی گیاهی پرمصرف است و به عناصر درشت‌مغذی (نیتروژن، فسفر، پتاسیم - NPK) و ریزمغذی‌ها (به ویژه کلسیم و منیزیم) نیاز دارد.
۱. مراحل مختلف کوددهی:
قبل از کاشت (آماده‌سازی خاک):
افزودن کود دامی پوسیده یا ورمی‌کمپوست جهت بهبود بافت خاک.
استفاده از کودهای پایه فسفر بالا (برای ریشه‌زایی) و پتاسیم.
مرحله رشد رویشی (قبل از گل‌دهی):
نیاز به نیتروژن (
𝑁
N
) برای رشد برگ‌ها و ساقه‌ها بیشتر است.
احتیاط: نیتروژن بیش از حد باعث رشد علفی گیاه شده و گل‌دهی را به تاخیر می‌اندازد. استفاده از کود متعادل مانند
20
20
20
202020
با غلظت مناسب توصیه می‌شود.
مرحله گل‌دهی و تشکیل میوه:
در این مرحله نیاز به نیتروژن کاهش و نیاز به فسفر (
𝑃
P
) و پتاسیم (
𝐾
K
) به شدت افزایش می‌یابد. پتاسیم برای کیفیت، اندازه و رنگ میوه ضروری است.
کودهای پتاس‌بالا (مانند
12
12
36
121236
) مناسب هستند.
مرحله رشد و رسیدن میوه:
ادامه تغذیه با پتاسیم بالا.
محلول‌پاشی کلسیم در این مرحله بسیار حیاتی است.
۲. عناصر کلیدی و ریزمغذی‌های ضروری:
کلسیم (
𝐶
𝑎
Ca
): کمبود کلسیم (یا عدم جذب آن به دلیل نوسانات آبیاری) باعث عارضه پوسیدگی گلگاه (سیاه شدن ته گوجه‌فرنگی) می‌شود. استفاده از کود نیترات کلسیم به صورت کودآبیاری یا محلول‌پاشی ضروری است.
منیزیم (
𝑀
𝑔
Mg
): کمبود آن باعث زرد شدن برگ‌های پیر (در حالی که رگبرگ‌ها سبز می‌مانند) می‌شود. سولفات منیزیم برای رفع این مشکل مفید است.
آهن (
𝐹
𝑒
Fe
) و روی (
𝑍
𝑛
Zn
): برای شادابی و فتوسنتز گیاه لازم هستند و معمولاً به صورت محلول‌پاشی یا کودهای کلاته استفاده می‌شوند.
خلاصه نکات طلایی پایگاه دانش:
پوسیدگی گلگاه: ترکیبی از کمبود کلسیم و آبیاری نامنظم است. همیشه رطوبت خاک را یکنواخت نگه دارید و از کلسیم استفاده کنید.
ترک‌خوردگی میوه: ناشی از تغییر ناگهانی رطوبت خاک (مثلاً آبیاری سنگین بعد از یک دوره خشکی) است.
تنظیم
𝑝
𝐻
pH
خاک: گوجه‌فرنگی در خاکی با
𝑝
𝐻
pH
بین
6.0
6.0
تا
6.8
6.8
بهترین جذب مواد مغذی را دارد.
فاصله کوددهی کلسیم و فسفر: کودهای حاوی کلسیم را هرگز با کودهای حاوی فسفر یا سولفات همزمان مخلوط نکنید (رسوب می‌کنند).
راهنمای کوددهی هویج
قاعده کلی برای هویج: هویج به نیتروژن (
𝑁
N
) کم تا متوسط، اما به فسفر (
𝑃
P
) و به ویژه پتاسیم (
𝐾
K
) بسیار بالایی نیاز دارد.
مراحل کوددهی:
آماده‌سازی خاک (قبل از کاشت): استفاده از کودهای پایه فسفر و پتاسیم. هشدار مهم: به هیچ وجه از کود دامی تازه استفاده نکنید! کود دامی باید کاملاً پوسیده باشد. کود حیوانی تازه باعث دو یا چند شاخه شدن هویج و ایجاد ریشه‌های مویی زائد می‌شود.
رشد رویشی (اوایل رشد): استفاده محدود از نیتروژن برای رشد برگ‌ها. نیتروژن بیش از حد باعث می‌شود گیاه تمام انرژی خود را صرف تولید برگ کند و ریشه (بخش خوراکی) نازک و کوچک بماند.
رشد و حجم گرفتن ریشه (اواسط تا اواخر رشد): استفاده از کودهای پتاس‌بالا (مانند سولوپتاس یا کودهای
12
12
36
121236
) برای افزایش سایز، بهبود رنگ، طعم شیرین‌تر و تردی هویج.
عناصر ریزمغذی کلیدی:
بُر (
𝐵
B
): یکی از مهم‌ترین عناصر برای هویج است. کمبود بُر باعث ایجاد شکاف در ریشه، سیاه شدن مغز هویج و کاهش بازارپسندی می‌شود.
کلسیم (
𝐶
𝑎
Ca
): برای استحکام بافت ریشه و جلوگیری از بیماری‌ها در انبار مهم است.
خلاصه نکات طلایی و مشکلات رایج
دو یا چند شاخه شدن هویج (Forking): ناشی از استفاده از کود دامی تازه، وجود سنگ و کلوخ در خاک، یا خاک‌های بسیار سفت و رسی است. خاک هویج باید تا عمق حداقل
25
25
سانتی‌متری پوک و سبک باشد.
ترک‌خوردگی ریشه: ناشی از نوسانات آبیاری (خاک خشک شود و ناگهان غرقاب گردد) یا دریافت بیش از حد نیتروژن در اواخر رشد.
ریشه‌های مویی فراوان روی هویج: ناشی از مصرف بیش از حد کودهای نیتروژنه (
𝑁
N
) یا رطوبت دائمی و بیش از حد خاک است.
تنظیم
𝑝
𝐻
pH
خاک: هویج در خاک‌هایی با
𝑝
𝐻
pH
بین
6.0
6.0
تا
6.8
6.8
بهترین رشد را دارد. در
𝑝
𝐻
pH
پایین‌تر (خاک اسیدی)، رشد ریشه متوقف می‌شود.
@@ -0,0 +1,28 @@
راهنمای استخراج برنامه کودهی از متن آزاد
هدف:
تبدیل توضیح متنی کشاورز درباره برنامه کودهی به JSON ساختاریافته.
اطلاعات کلیدی که معمولا باید استخراج شوند:
- نام محصول
- مرحله رشد
- هدف مصرف
- نام یا فرمول کود
- مقدار مصرف
- روش مصرف
- زمان مصرف
- فاصله بین نوبت ها
- توضیح تکمیلی یا هشدار
نمونه عبارت های رایج:
- هر 10 روز یک بار
- بعد از آبیاری
- به صورت کودآبیاری
- سرک
- محلول پاشی
- 35 کیلوگرم در هکتار
- 20-20-20
- برای تقویت رشد رویشی
- برای شروع گلدهی
اگر متن ناقص بود، باید فقط سوال های لازم برای تکمیل برنامه نهایی پرسیده شود و از حدس زدن خودداری شود.
@@ -0,0 +1,26 @@
بخش اول: راهنمای آبیاری گوجه‌فرنگی (آب‌دهی)
نیاز آبی گوجه‌فرنگی به مرحله رشد، نوع خاک و شرایط آب و هوایی بستگی دارد. مهم‌ترین اصل در آبیاری گوجه‌فرنگی نظم و یکنواختی است.
۱. مراحل مختلف رشد و نیاز آبی:
مرحله نشاء و رشد اولیه: خاک باید مرطوب (نه غرقاب) نگه داشته شود تا ریشه‌ها به خوبی مستقر شوند. آبیاری سطحی و مکرر توصیه می‌شود.
مرحله گل‌دهی: تنش آبی در این مرحله باعث ریزش گل‌ها می‌شود. آبیاری باید منظم باشد.
مرحله تشکیل و بزرگ شدن میوه: بیشترین نیاز آبی در این مرحله است. آبیاری باید عمیق و منظم باشد تا از مشکلاتی مانند ترک‌خوردگی میوه و پوسیدگی گلگاه جلوگیری شود.
مرحله رسیدن میوه: با شروع رنگ گرفتن گوجه‌ها، آبیاری را کمی کاهش دهید. این کار باعث افزایش قند، بهبود طعم و جلوگیری از ترک خوردن میوه می‌شود.
۲. نکات کلیدی در آبیاری:
روش آبیاری: بهترین روش، آبیاری قطره‌ای است. آبیاری بارانی باعث خیس شدن برگ‌ها و افزایش خطر بیماری‌های قارچی می‌شود.
زمان آبیاری: بهترین زمان، صبح زود است تا گیاه در طول روز رطوبت کافی داشته باشد و برگ‌ها تا شب خشک شوند.
عمق آبیاری: آبیاری باید عمیق باشد تا ریشه‌ها به عمق خاک نفوذ کنند (حداقل ۱۵ تا ۲۰ سانتی‌متر).
مالچ‌پاشی: استفاده از مالچ (پلاستیک کشاورزی یا کاه و کلش) روی خاک، رطوبت را حفظ کرده و از تبخیر سریع آب جلوگیری می‌کند.
راهنمای آبیاری هویج
اهمیت رطوبت در هویج: هویج یک گیاه ریشه‌ای است و کیفیت ریشه آن ارتباط مستقیمی با نحوه آبیاری دارد. نوسانات رطوبتی باعث افت شدید کیفیت محصول می‌شود.
نیاز آبی در مراحل مختلف رشد:
کاشت و جوانه‌زنی: بذر هویج بسیار ریز است و در عمق کم کاشته می‌شود. در این مرحله خاک باید دائماً مرطوب (اما نه غرقاب) باشد تا بذرها خشک نشوند. خشکی در این مرحله باعث عدم سبز شدن بذرها می‌شود.
رشد اولیه و توسعه ریشه: پس از سبز شدن، آبیاری باید عمیق‌تر و با فواصل بیشتر انجام شود تا ریشه گیاه برای پیدا کردن آب به عمق خاک نفوذ کند. آبیاری سطحی باعث کوتاه ماندن هویج می‌شود.
حجم گرفتن ریشه (غده‌بندی): نیاز آبی در این مرحله بالاست. رطوبت باید یکنواخت باشد.
نزدیک به برداشت: کاهش آبیاری در اواخر دوره رشد ضروری است. آبیاری زیاد در این مرحله باعث ترک‌خوردگی هویج‌ها می‌شود.
روش‌های آبیاری:
بهترین روش: آبیاری قطره‌ای (نوار تیپ) زیرا رطوبت را به صورت یکنواخت در اختیار ریشه قرار می‌دهد و از بیماری‌های برگی جلوگیری می‌کند.
تنش آبی: خشک و خیس شدن پیاپی خاک، عامل اصلی دو شاخه شدن و ترک خوردن هویج است.
@@ -0,0 +1,28 @@
راهنمای استخراج برنامه آبیاری از متن آزاد
هدف:
تبدیل توضیح متنی کشاورز درباره برنامه آبیاری به JSON ساختاریافته.
اطلاعات کلیدی که معمولا باید استخراج شوند:
- نام محصول
- مرحله رشد
- روش آبیاری
- مقدار آب در هر نوبت
- مدت زمان هر نوبت
- فاصله یا تعداد دفعات آبیاری
- زمان مناسب اجرا در روز
- تاریخ شروع یا شرایط شروع
- ناحیه یا سطح هدف
- نکات تکمیلی
نمونه عبارت های رایج:
- هر سه روز یک بار
- هفته ای دو نوبت
- صبح زود
- بعد از غروب
- 20 لیتر برای هر بوته
- 25 دقیقه
- فقط در ردیف های جنوبی
- اگر هوا خیلی گرم شد یک نوبت اضافه شود
اگر متن ناقص بود، باید فقط درباره اطلاعاتی سوال شود که برای ساخت برنامه قابل استفاده لازم هستند.
@@ -0,0 +1,13 @@
این پایگاه دانش برای تحلیل آفات و بیماری های گیاهی استفاده می شود.
قواعد اصلی:
1. فقط بر اساس شواهد تصویری، داده های مزرعه، و اطلاعات بازیابی شده نتیجه گیری کن.
2. اگر تصویر برای تشخیص قطعی کافی نیست، عدم قطعیت را شفاف بگو.
3. تشخیص باید بین این حالت ها تفکیک کند: no_issue, pest, disease, nutrient_stress, abiotic_stress, unknown.
4. در تحلیل تصویری، نشانه های قابل مشاهده مثل لکه، پوسیدگی، پیچیدگی برگ، سوراخ شدگی، تغییر رنگ، کپک، یا آفت قابل مشاهده ذکر شود.
5. در پیش بینی ریسک، شرایط دما، رطوبت، بارش، رطوبت خاک، pH، EC، و مرحله رشد لحاظ شوند.
6. سطح ریسک فقط یکی از low, medium, high باشد.
7. اقدام های پیشنهادی باید کوتاه، عملیاتی، و محافظه کارانه باشند و از ارائه نسخه درمان قطعی بدون داده کافی خودداری شود.
8. اگر آلودگی قارچی محتمل است، به رطوبت بالا و ماندگاری رطوبت اشاره کن.
9. اگر فشار آفت محتمل است، به گرما، خشکی، ضعف گیاه، و الگوی خسارت برگ اشاره کن.
10. همیشه خلاصه ای از دلیل نتیجه گیری ارائه بده.
@@ -0,0 +1,23 @@
تحليل ناهنجاري خاک و سنسور
هدف اين دانشنامه کمک به تفسير ناهنجاري هاي آماري در داده هاي خاک و سنسور مزرعه است.
اصول کلي:
- ناهنجاري آماري به معناي مشکل قطعي مزرعه نيست؛ اول بايد پايداري رخداد، شدت انحراف، و سازگاري آن با ساير شاخص ها بررسي شود.
- وقتي رطوبت خاک و دماي خاک همزمان ناهنجار مي شوند، احتمال تنش ريشه، آبياري نامناسب، يا موج گرما بيشتر است.
- وقتي EC و رطوبت خاک با هم ناهنجار شوند، فشار شوري، تجمع نمک، کيفيت نامناسب آب يا برنامه کوددهي نامتوازن بايد بررسي شود.
- اگر pH از محدوده معمول مزرعه فاصله بگيرد، دسترسي عناصر غذايي و کارايي جذب ريشه مي تواند تحت تاثير قرار بگيرد.
- ناهنجاري رطوبت هوا در کنار دما و رطوبت خاک مي تواند نشانه شرايط مساعد براي بيماري يا افزايش تبخير-تعرق باشد.
راهنماي تفسير شدت:
- low: انحراف خفيف يا کوتاه مدت؛ معمولا نياز به پايش دارد.
- medium: انحراف قابل توجه؛ بايد با شرايط مزرعه و آبياري تطبيق داده شود.
- high: انحراف مهم؛ بازبيني سريع سنسور و عمليات مزرعه لازم است.
- critical: رخداد شديد يا پرتکرار؛ نياز به اقدام فوري و بررسي ميداني دارد.
اقدامات پيشنهادي عمومي:
- وضعيت آخرين آبياري، زمان بندي و يکنواختي توزيع آب بررسي شود.
- کاليبراسيون سنسور و سلامت سخت افزاري آن در رخدادهاي ناگهاني کنترل شود.
- تغييرات اخير در کوددهي، شوري آب، بارش موثر و دماي محيط در تحليل لحاظ شود.
- اگر ناهنجاري در چند شاخص همزمان ديده شد، اولويت پايش و مداخله بالاتر در نظر گرفته شود.
- اگر ناهنجاري در داده هاي محدود يا ناقص ديده شد، قبل از توصيه قطعي کمبود داده صريح گفته شود.
@@ -0,0 +1,19 @@
# دانش پایه خاک برای کشاورزی
## انواع خاک
خاک‌ها بر اساس بافت (نسبت رس، سیلت و شن) دسته‌بندی می‌شوند. خاک رسی زهکشی ضعیف‌تری دارد و خاک شنی زهکشی سریع. خاک لومی ترکیبی متعادل از هر سه است و برای اغلب گیاهان مناسب است.
## pH خاک
مقیاس pH از ۰ تا ۱۴ است؛ مقدار ۷ خنثی است. خاک‌های اسیدی (زیر ۷) و قلیایی (بالای ۷) بر جذب عناصر غذایی تأثیر می‌گذارند. بیشتر گیاهان زراعی pH حدود ۶ تا ۷.۵ را ترجیح می‌دهند.
## رطوبت خاک
رطوبت خاک بر رشد ریشه و جذب آب و مواد غذایی تأثیر مستقیم دارد. رطوبت بیش از حد باعث خفگی ریشه و کمبود اکسیژن می‌شود؛ رطوبت کم باعث تنش آبی و کاهش عملکرد می‌شود.
## NPK و عناصر غذایی
نیتروژن (N) برای رشد سبزینه و برگ‌ها ضروری است. فسفر (P) برای ریشه‌زایی و گلدهی مهم است. پتاسیم (K) مقاومت به خشکی و بیماری را افزایش می‌دهد. مقادیر این عناصر در خاک با آزمون خاک قابل اندازه‌گیری است.
## هدایت الکتریکی (EC)
EC نشان‌دهنده شوری خاک است. EC بالا یعنی نمک زیاد و می‌تواند به ریشه گیاه آسیب برساند. واحد آن معمولاً dS/m یا mS/cm است.
## عمق خاک
داده‌های خاک معمولاً در اعماق ۰–۵، ۵–۱۵ و ۱۵–۳۰ سانتی‌متر اندازه‌گیری می‌شوند. لایه سطحی برای جوانه‌زنی و ریشه‌های سطحی مهم است؛ لایه‌های عمیق‌تر برای گیاهان ریشه‌عمیق اهمیت دارند.
@@ -0,0 +1,17 @@
تحليل نياز آبي کوتاه مدت مزرعه
اين دانشنامه براي تفسير خروجي محاسبات نياز آبي روزهاي آينده استفاده مي شود.
اصول کلي:
- `et0` تبخير-تعرق مرجع است و نشان مي دهد شرايط اقليمي هر روز چه ميزان تقاضاي تبخير-تعرق ايجاد مي کند.
- `etc` از ضرب `et0` در ضريب گياهي `kc` به دست مي آيد و تخمين مناسب تري از نياز آبي محصول مي دهد.
- `effective_rainfall` بخشي از بارش است که واقعا در تامين نياز آبي گياه موثر واقع مي شود.
- `net_irrigation_mm` نياز آبي خالص پس از کسر بارش موثر است.
- `gross_irrigation_mm` نياز آبي واقعي اجرايي با درنظر گرفتن راندمان سامانه آبياري است.
راهنماي تفسير:
- اگر `gross_irrigation_mm` در چند روز پياپي بالا باشد، برنامه آبياري بايد فشرده تر و منظم تر تنظيم شود.
- اگر راندمان آبياري پايين باشد، اختلاف بين نياز خالص و ناخالص بيشتر مي شود و اتلاف آب بالاتر است.
- در روزهاي گرم، پر باد يا کم بارش، بهتر است اجراي آبياري به صبح زود يا نزديک غروب منتقل شود.
- اگر بارش موثر پيش بيني شده باشد، بخشي از نياز آبي مي تواند بدون آبياري اضافي تامين شود.
- توصيه ها بايد عملياتي، کوتاه مدت، و همسو با forecast فعلي باشند و در صورت عدم قطعيت، آن را صريح بيان کنند.
+103
View File
@@ -0,0 +1,103 @@
import copy
from drf_spectacular.utils import OpenApiResponse
from rest_framework import serializers
def _build_schema_field(schema, *, many=False, required=True, allow_null=False):
if schema is None:
return serializers.JSONField(required=required, allow_null=allow_null)
if isinstance(schema, serializers.Field):
field = copy.deepcopy(schema)
field.required = required
if hasattr(field, "allow_null"):
field.allow_null = allow_null
return field
if isinstance(schema, serializers.BaseSerializer):
serializer = copy.deepcopy(schema)
serializer.required = required
serializer.allow_null = allow_null
return serializer
if isinstance(schema, type) and issubclass(schema, serializers.BaseSerializer):
return schema(many=many, required=required, allow_null=allow_null)
raise TypeError(f"Unsupported schema type: {type(schema)!r}")
def build_message_response_serializer(name):
return type(
name,
(serializers.Serializer,),
{
"__module__": __name__,
"code": serializers.IntegerField(),
"msg": serializers.CharField(),
},
)
def build_envelope_serializer(
name,
data_schema=None,
*,
many=False,
data_required=True,
allow_null=False,
):
return type(
name,
(serializers.Serializer,),
{
"__module__": __name__,
"code": serializers.IntegerField(),
"msg": serializers.CharField(),
"data": _build_schema_field(
data_schema,
many=many,
required=data_required,
allow_null=allow_null,
),
},
)
def build_task_queue_data_serializer(name, extra_fields=None):
fields = {
"__module__": __name__,
"task_id": serializers.CharField(),
"status_url": serializers.CharField(),
}
if extra_fields:
fields.update(extra_fields)
return type(name, (serializers.Serializer,), fields)
def build_task_status_data_serializer(name, result_schema=None):
result_field = (
_build_schema_field(result_schema, required=False, allow_null=True)
if result_schema is not None
else serializers.JSONField(required=False)
)
return type(
name,
(serializers.Serializer,),
{
"__module__": __name__,
"task_id": serializers.CharField(),
"status": serializers.CharField(),
"message": serializers.CharField(required=False),
"progress": serializers.DictField(
child=serializers.JSONField(),
required=False,
),
"result": result_field,
"error": serializers.CharField(required=False),
},
)
def build_response(serializer, description):
return OpenApiResponse(response=serializer, description=description)
+239
View File
@@ -0,0 +1,239 @@
# تنظیمات RAG برای پایگاه دانش CropLogic
embedding:
provider: "arvancloud" # gapgpt یا avalai یا arvancloud
model: "Bge-m3-smka5"
base_url: "https://api.gapgpt.app/v1"
api_key_env: "GAPGPT_API_KEY"
batch_size: 32
# تنظیمات Avalai (برای fallback)
avalai_base_url: "https://api.avalai.ir/v1"
avalai_api_key_env: "AVALAI_API_KEY"
# تنظیمات ArvanCloud AI برای BGE-M3
arvancloud_api_key: "7c4c4eb9-5183-530a-b589-d31c79472847"
arvancloud_base_url: "https://arvancloudai.ir/gateway/models/Bge-m3/rBA2PgcTC2sfhXwamupI4NvQ8crddUGTYXOsuKVye91PoNuGhbRgpHHNY8sMHBVQWWerZSAi4a0AijUL6YBqY9EW-Y1LhW_0ec6Mxr85GQy41lXiV6M8Od4mvLIeDF-wLRUHIervod0O5ZqGj2MOX8z1zdUpXkCrIS2uDjHlfHBZofledZjsOVDmFZU7IYfvkA__ljQqNeKXSFgpwUR7SmsbRUXGTDB2moLdeRq9zBpQIw/v1"
arvancloud_api_key_env: "ARVANCLOUD_EMBEDDING_API_KEY"
# فاز یک: Qdrant به‌عنوان vector store
qdrant:
host: "localhost" # یا qdrant در Docker
port: 6333
collection_name: "croplogic_kb"
vector_size: 1024 # متناسب با BGE-M3
chunking:
max_chunk_tokens: 500
overlap_tokens: 50
# تنظیمات مدل چت (LLM) — Avalai
llm:
provider: "gapgpt"
model: "gpt-4o"
base_url: "https://api.gapgpt.app/v1"
api_key_env: "GAPGPT_API_KEY"
avalai_base_url: "https://api.avalai.ir/v1"
avalai_api_key_env: "AVALAI_API_KEY"
# سه پایگاه دانش مجزا
knowledge_bases:
chat:
path: "config/knowledge_base/chat"
tone_file: "config/tones/chat_tone.txt"
description: "پایگاه دانش عمومی برای چت با کاربران"
irrigation:
path: "config/knowledge_base/irrigation"
tone_file: "config/tones/irrigation_tone.txt"
description: "پایگاه دانش توصیه آبیاری"
fertilization:
path: "config/knowledge_base/fertilization"
tone_file: "config/tones/fertilization_tone.txt"
description: "پایگاه دانش توصیه کودهی"
irrigation_plan_parser:
path: "config/knowledge_base/irrigation_plan_parser"
tone_file: "config/tones/irrigation_plan_parser_tone.txt"
description: "پایگاه دانش استخراج برنامه آبیاری از متن آزاد کاربر"
fertilization_plan_parser:
path: "config/knowledge_base/fertilization_plan_parser"
tone_file: "config/tones/fertilization_plan_parser_tone.txt"
description: "پایگاه دانش استخراج برنامه کودهی از متن آزاد کاربر"
farm_alerts:
path: "config/knowledge_base/farm_alerts"
tone_file: "config/tones/farm_alerts_tone.txt"
description: "پایگاه دانش تحلیل هشدار و اعلان مزرعه"
pest_disease:
path: "config/knowledge_base/pest_disease"
tone_file: "config/tones/pest_disease_tone.txt"
description: "پایگاه دانش تشخیص و پیش بینی آفات و بیماری گیاهی"
soil_anomaly:
path: "config/knowledge_base/soil_anomaly"
tone_file: "config/tones/soil_anomaly_tone.txt"
description: "پایگاه دانش تحلیل ناهنجاری آماری داده های خاک و سنسور"
water_need_prediction:
path: "config/knowledge_base/water_need_prediction"
tone_file: "config/tones/water_need_prediction_tone.txt"
description: "پایگاه دانش تفسير نياز آبي کوتاه مدت و برنامه ريزي آبياري"
yield_harvest:
path: "config/knowledge_base/chat"
tone_file: "config/tones/yield_harvest_tone.txt"
description: "پایگاه دانش روایت کاربرپسند برای داشبورد Yield & Harvest Summary"
services:
support_bot:
knowledge_base: "chat"
tone_file: "config/tones/chat_tone.txt"
use_user_embeddings: false
description: "سرویس پشتیبانی عمومی"
llm:
provider: "gapgpt"
model: "gpt-4o"
base_url: "https://api.gapgpt.app/v1"
api_key_env: "GAPGPT_API_KEY"
avalai_base_url: "https://api.avalai.ir/v1"
avalai_api_key_env: "AVALAI_API_KEY"
system_prompt: "You are a friendly support assistant. Answer clearly and helpfully."
chat:
knowledge_base: "chat"
tone_file: "config/tones/chat_tone.txt"
use_user_embeddings: true
description: "چت عمومی با داده‌های کاربر"
llm:
provider: "gapgpt"
model: "gpt-4o"
base_url: "https://api.gapgpt.app/v1"
api_key_env: "GAPGPT_API_KEY"
avalai_base_url: "https://api.avalai.ir/v1"
avalai_api_key_env: "AVALAI_API_KEY"
irrigation:
knowledge_base: "irrigation"
tone_file: "config/tones/irrigation_tone.txt"
use_user_embeddings: true
description: "سرویس توصیه آبیاری"
llm:
provider: "gapgpt"
model: "gpt-4o"
base_url: "https://api.gapgpt.app/v1"
api_key_env: "GAPGPT_API_KEY"
avalai_base_url: "https://api.avalai.ir/v1"
avalai_api_key_env: "AVALAI_API_KEY"
fertilization:
knowledge_base: "fertilization"
tone_file: "config/tones/fertilization_tone.txt"
use_user_embeddings: true
description: "سرویس توصیه کودهی"
llm:
provider: "gapgpt"
model: "gpt-4o"
base_url: "https://api.gapgpt.app/v1"
api_key_env: "GAPGPT_API_KEY"
avalai_base_url: "https://api.avalai.ir/v1"
avalai_api_key_env: "AVALAI_API_KEY"
irrigation_plan_parser:
knowledge_base: "irrigation_plan_parser"
tone_file: "config/tones/irrigation_plan_parser_tone.txt"
use_user_embeddings: false
description: "سرویس استخراج برنامه آبیاری از متن کاربر"
system_prompt: "Only return valid JSON for irrigation plan extraction and clarification."
llm:
provider: "gapgpt"
model: "gpt-4o"
base_url: "https://api.gapgpt.app/v1"
api_key_env: "GAPGPT_API_KEY"
avalai_base_url: "https://api.avalai.ir/v1"
avalai_api_key_env: "AVALAI_API_KEY"
fertilization_plan_parser:
knowledge_base: "fertilization_plan_parser"
tone_file: "config/tones/fertilization_plan_parser_tone.txt"
use_user_embeddings: false
description: "سرویس استخراج برنامه کودهی از متن کاربر"
system_prompt: "Only return valid JSON for fertilization plan extraction and clarification."
llm:
provider: "gapgpt"
model: "gpt-4o"
base_url: "https://api.gapgpt.app/v1"
api_key_env: "GAPGPT_API_KEY"
avalai_base_url: "https://api.avalai.ir/v1"
avalai_api_key_env: "AVALAI_API_KEY"
farm_alerts:
knowledge_base: "farm_alerts"
tone_file: "config/tones/farm_alerts_tone.txt"
use_user_embeddings: true
description: "سرویس تحلیل tracker و timeline هشدارهای مزرعه"
llm:
provider: "gapgpt"
model: "gpt-4o"
base_url: "https://api.gapgpt.app/v1"
api_key_env: "GAPGPT_API_KEY"
avalai_base_url: "https://api.avalai.ir/v1"
avalai_api_key_env: "AVALAI_API_KEY"
pest_disease:
knowledge_base: "pest_disease"
tone_file: "config/tones/pest_disease_tone.txt"
use_user_embeddings: true
description: "سرویس تشخیص و پیش بینی آفات و بیماری"
llm:
provider: "gapgpt"
model: "gpt-4o"
base_url: "https://api.gapgpt.app/v1"
api_key_env: "GAPGPT_API_KEY"
avalai_base_url: "https://api.avalai.ir/v1"
avalai_api_key_env: "AVALAI_API_KEY"
soil_anomaly:
knowledge_base: "soil_anomaly"
tone_file: "config/tones/soil_anomaly_tone.txt"
use_user_embeddings: true
description: "سرویس تفسير ناهنجاري هاي آماري خاک و سنسور"
llm:
provider: "gapgpt"
model: "gpt-4o"
base_url: "https://api.gapgpt.app/v1"
api_key_env: "GAPGPT_API_KEY"
avalai_base_url: "https://api.avalai.ir/v1"
avalai_api_key_env: "AVALAI_API_KEY"
water_need_prediction:
knowledge_base: "water_need_prediction"
tone_file: "config/tones/water_need_prediction_tone.txt"
use_user_embeddings: true
description: "سرویس تفسير نياز آبي کوتاه مدت مزرعه"
llm:
provider: "gapgpt"
model: "gpt-4o"
base_url: "https://api.gapgpt.app/v1"
api_key_env: "GAPGPT_API_KEY"
avalai_base_url: "https://api.avalai.ir/v1"
avalai_api_key_env: "AVALAI_API_KEY"
yield_harvest:
knowledge_base: "yield_harvest"
tone_file: "config/tones/yield_harvest_tone.txt"
use_user_embeddings: true
description: "سرویس روایت داشبورد عملکرد و برداشت"
fallback_behavior:
on_invalid_json: "raise_validation_error"
on_missing_context: "use_only_deterministic_data"
on_number_conflict: "prefer_deterministic_data"
prompt_template: "config/tones/yield_harvest_tone.txt"
llm:
provider: "gapgpt"
model: "gpt-4o"
base_url: "https://api.gapgpt.app/v1"
api_key_env: "GAPGPT_API_KEY"
avalai_base_url: "https://api.avalai.ir/v1"
avalai_api_key_env: "AVALAI_API_KEY"
+250
View File
@@ -0,0 +1,250 @@
import os
import importlib.util
from pathlib import Path
from django.core.exceptions import ImproperlyConfigured
try:
from dotenv import load_dotenv
except ImportError: # pragma: no cover - optional in stripped test envs
def load_dotenv():
return False
load_dotenv()
BASE_DIR = Path(__file__).resolve().parent.parent
LOG_DIR = Path(os.environ.get("LOG_DIR", BASE_DIR / "logs"))
def _can_use_file_logging(log_dir: Path) -> bool:
try:
log_dir.mkdir(parents=True, exist_ok=True)
probe_file = log_dir / ".write_test"
with probe_file.open("a", encoding="utf-8"):
pass
probe_file.unlink(missing_ok=True)
return True
except OSError:
return False
FILE_LOGGING_ENABLED = _can_use_file_logging(LOG_DIR)
SECRET_KEY = os.environ.get("SECRET_KEY", "django-insecure-dev-only")
DEBUG = os.environ.get("DEBUG", "0") == "1"
DEVELOP = os.environ.get("DEVELOP", "false").strip().lower() in {"1", "true", "yes", "on"}
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"farm_alerts.apps.FarmAlertsConfig",
"rag",
"location_data",
"soile.apps.SoileConfig",
"farm_data.apps.FarmDataConfig",
"weather",
"economy.apps.EconomyConfig",
"plant",
"pest_disease.apps.PestDiseaseConfig",
"irrigation",
"fertilization",
"crop_simulation.apps.CropSimulationConfig",
]
for optional_app in [
"rest_framework",
"corsheaders",
"drf_spectacular",
"drf_spectacular_sidecar",
]:
if importlib.util.find_spec(optional_app):
INSTALLED_APPS.insert(6, optional_app)
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
if importlib.util.find_spec("corsheaders"):
MIDDLEWARE.insert(1, "corsheaders.middleware.CorsMiddleware")
if importlib.util.find_spec("whitenoise"):
MIDDLEWARE.insert(1, "whitenoise.middleware.WhiteNoiseMiddleware")
ROOT_URLCONF = "config.urls"
WSGI_APPLICATION = "config.wsgi.application"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
DATABASES = {
"default": {
"ENGINE": os.environ.get("DB_ENGINE", "django.db.backends.mysql"),
"NAME": os.environ.get("DB_NAME", "ai"),
"USER": os.environ.get("DB_USER", "ai"),
"PASSWORD": os.environ.get("DB_PASSWORD", ""),
"HOST": os.environ.get("DB_HOST", "127.0.0.1"),
"PORT": os.environ.get("DB_PORT", "3306"),
}
}
if DATABASES["default"]["ENGINE"].endswith("mysql"):
DATABASES["default"]["OPTIONS"] = {"charset": "utf8mb4"}
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
if importlib.util.find_spec("whitenoise"):
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "croplogic-auth-otp",
}
}
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.AllowAny",
],
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}
SPECTACULAR_SETTINGS = {
"TITLE": "CropLogic AI API",
"DESCRIPTION": "Swagger/OpenAPI documentation for all CropLogic AI API endpoints.",
"VERSION": "1.0.0",
"SERVE_INCLUDE_SCHEMA": False,
"SWAGGER_UI_DIST": "SIDECAR",
"SWAGGER_UI_FAVICON_HREF": "SIDECAR",
"REDOC_DIST": "SIDECAR",
"SCHEMA_PATH_PREFIX": r"/api/",
"SERVE_PERMISSIONS": ["rest_framework.permissions.AllowAny"],
"SWAGGER_UI_SETTINGS": {
"persistAuthorization": True,
},
}
CORS_ALLOW_ALL_ORIGINS = DEBUG
# Celery
CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "redis://localhost:6379/0")
CELERY_RESULT_BACKEND = os.environ.get("CELERY_RESULT_BACKEND", "redis://localhost:6379/0")
CELERY_ACCEPT_CONTENT = ["json"]
CELERY_TASK_SERIALIZER = "json"
# Celery Beat — embed دیتای کاربران هر ۶ ساعت
CELERY_BEAT_SCHEDULE = {
"rag-ingest-periodic": {
"task": "rag.tasks.rag_ingest_task",
"schedule": 6 * 60 * 60, # ۶ ساعت
},
"weather-fetch-periodic": {
"task": "weather.tasks.fetch_weather_all_locations_task",
"schedule": 6 * 60 * 60, # ۶ ساعت
},
}
# Weather API
WEATHER_API_BASE_URL = os.environ.get(
"WEATHER_API_BASE_URL", "https://api.open-meteo.com/v1/forecast"
)
WEATHER_API_KEY = os.environ.get("WEATHER_API_KEY", "")
WEATHER_DATA_PROVIDER = os.environ.get("WEATHER_DATA_PROVIDER", "open-meteo").strip().lower()
WEATHER_MOCK_DELAY_SECONDS = float(os.environ.get("WEATHER_MOCK_DELAY_SECONDS", "0.8"))
WEATHER_TIMEOUT_SECONDS = float(os.environ.get("WEATHER_TIMEOUT_SECONDS", "60"))
SOIL_DATA_PROVIDER = os.environ.get("SOIL_DATA_PROVIDER", "soilgrids").strip().lower()
SOIL_MOCK_DELAY_SECONDS = float(os.environ.get("SOIL_MOCK_DELAY_SECONDS", "0.8"))
SOILGRIDS_TIMEOUT_SECONDS = float(os.environ.get("SOILGRIDS_TIMEOUT_SECONDS", "60"))
SUBDIVISION_CHUNK_SQM = int(os.environ.get("SUBDIVISION_CHUNK_SQM", "900"))
BACKEND_PLANT_SYNC_BASE_URL = os.environ.get("BACKEND_PLANT_SYNC_BASE_URL", "")
BACKEND_PLANT_SYNC_API_KEY = os.environ.get("BACKEND_PLANT_SYNC_API_KEY", "")
BACKEND_PLANT_SYNC_TIMEOUT = int(os.environ.get("BACKEND_PLANT_SYNC_TIMEOUT", "20"))
if not (DEBUG or DEVELOP):
if WEATHER_DATA_PROVIDER == "mock":
raise ImproperlyConfigured("WEATHER_DATA_PROVIDER=mock is allowed only in dev/test environments.")
if SOIL_DATA_PROVIDER == "mock":
raise ImproperlyConfigured("SOIL_DATA_PROVIDER=mock is allowed only in dev/test environments.")
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "standard",
},
},
"loggers": {
"django": {
"handlers": ["console"],
"level": os.environ.get("DJANGO_LOG_LEVEL", "INFO"),
"propagate": False,
},
"rag": {
"handlers": ["console"],
"level": os.environ.get("RAG_LOG_LEVEL", "INFO"),
"propagate": False,
},
},
"root": {
"handlers": ["console"],
"level": os.environ.get("ROOT_LOG_LEVEL", "INFO"),
},
}
if FILE_LOGGING_ENABLED:
LOGGING["handlers"]["file"] = {
"class": "logging.handlers.TimedRotatingFileHandler",
"filename": str(LOG_DIR / "app.log"),
"when": "midnight",
"backupCount": 14,
"encoding": "utf-8",
"formatter": "standard",
}
LOGGING["loggers"]["django"]["handlers"].append("file")
LOGGING["loggers"]["rag"]["handlers"].append("file")
LOGGING["root"]["handlers"].append("file")
+15
View File
@@ -0,0 +1,15 @@
from .settings import * # noqa: F403,F401
ROOT_URLCONF = "config.test_urls"
LOGGING = { # noqa: F405
"version": 1,
"disable_existing_loggers": False,
"handlers": {"console": {"class": "logging.StreamHandler"}},
"root": {"handlers": ["console"], "level": "WARNING"},
}
REST_FRAMEWORK = { # noqa: F405
**REST_FRAMEWORK, # noqa: F405
"DEFAULT_AUTHENTICATION_CLASSES": [],
}
+15
View File
@@ -0,0 +1,15 @@
from django.http import HttpResponse
from django.urls import include, path
def test_view(_request):
return HttpResponse("ok")
urlpatterns = [
path("__test__/", test_view),
path("api/rag/", include("rag.urls")),
path("api/farm-alerts/", include("farm_alerts.urls")),
path("api/pest-disease/", include("pest_disease.urls")),
path("api/farm-data/", include("farm_data.urls")),
]
+7
View File
@@ -0,0 +1,7 @@
# فایل لحن / سبک پاسخ‌های RAG
لحن و سبک پاسخ‌ها:
- سطح: دوستانه و تخصصی؛ با کشاورز به زبان ساده و علمی صحبت کن.
- واژگان: از اصطلاحات رایج کشاورزی و خاک‌شناسی استفاده کن، در صورت نیاز معادل فارسی بیاور.
- طول: پاسخ‌ها مختصر و کاربردی؛ در صورت لزوم با بولت یا شماره ساختاربندی کن.
- هشدار: اگر موضوع ایمنی یا سلامتی گیاه/خاک باشد، صریحاً هشدار بده.
+152
View File
@@ -0,0 +1,152 @@
You are a general farm assistant for CropLogic.
## GOAL
Generate a Persian response that fits the CropLogic frontend.
Stay strictly relevant to the user's intent.
Support three UI output modes based on the users need:
- pureText
- textOnly (light explanation card)
- actionCard (full recommendation card)
## HARD RULES
1) If an optimizer block exists, it is the single source of truth.
2) Never produce actions unless the user asks OR a clear critical issue exists.
3) Output must be exactly one JSON object with a top-level "sections" array.
4) No text outside JSON.
## INTENT CLASSIFICATION
Determine user intent as one of:
- "pure_info" → کاربر فقط اطلاعات یا توضیح می‌خواهد (مثال: «قبلاً کاهو و پیاز کاشتم، تأثیرش چیه؟»)
- "diagnostic_or_info" → کاربر دلیل یا ماهیت را می‌پرسد («چرا برگ زرد شده؟»)
- "advisory_or_operational" → کاربر اقدام و توصیه می‌خواهد («چه کودی بدم؟»)
---
# UI MODES (۳ حالت)
### 1) uiMode = "pureText"
استفاده شود وقتی:
- intent = pure_info
و هیچ نیازی به کارت توصیه یا لیست وجود ندارد.
فرانت باید فقط یک متن ساده نمایش دهد.
در این حالت:
sections = [
{
"type": "pureText",
"content": "متن کامل و یکپارچه پاسخ"
}
]
هیچ recommendation، list یا warning نباید وجود داشته باشد.
---
### 2) uiMode = "textOnly"
استفاده شود وقتی:
- intent = diagnostic_or_info
- نیازی به اقدام عملی نیست
- اما ساختار کارت سبک لازم است
در این حالت:
- type = "recommendation"
- uiMode = "textOnly"
- content = متن اصلی (۲–۴ جمله)
- primaryAction, timing, validityPeriod = null
- expandableExplanation = توضیحات اختیاری
---
### 3) uiMode = "actionCard"
استفاده شود وقتی:
- intent = advisory_or_operational
یا
- یک مشکل بحرانی وجود دارد (براساس داده)
در این حالت:
- content = خلاصه کوتاه
- expandableExplanation = توضیح کامل
- primaryAction/timing/validityPeriod → مقدار مناسب
همچنین چند recommendation و چند warning مجاز است.
---
# MULTIPLE RECOMMENDATION & WARNING
- Any number of "recommendation" cards allowed
- Any number of "warning" cards allowed
- Each must be a separate object in "sections"
---
# DATA USE RULES
Use data only when relevant.
If data missing → create a warning section.
---
# OUTPUT CONTRACT
### حالت pureText
{
"sections": [
{
"type": "pureText",
"content": "متن کامل و ساده پاسخ"
}
]
}
### حالت textOnly یا actionCard
{
"sections": [
{
"type": "recommendation",
"uiMode": "textOnly | actionCard",
"title": "جمع بندی اصلی",
"icon": "message-circle",
"content": "string",
"primaryAction": "string|null",
"timing": "string|null",
"validityPeriod": "string|null",
"expandableExplanation": "string|null"
},
{
"type": "list",
"title": "نکات اجرایی یا بررسی",
"icon": "list",
"items": ["string", "string"]
},
{
"type": "warning",
"title": "هشدار یا محدودیت",
"icon": "alert-triangle",
"content": "string"
}
]
}
---
# WRITING RULES
- No markdown
- No greetings
- No external chatter
- Response must be fully inside the JSON
- Focus exactly on the user's question
- Never force farm actions unless needed
---
# CHAT TITLE RULE
- Always include a separate section at the start of "sections" for the chat title.
- The title section must be completely separate from the answer section.
- Use this exact structure for the first section:
{
"type": "chatTitle",
"title": "یک عنوان کوتاه، طبیعی، و مرتبط با سوال کاربر"
}
- After the title section, return the actual answer sections.
- Never merge the chat title into a recommendation, warning, list, or pureText section.
@@ -0,0 +1,65 @@
شما دستيار تخصصي تحليل هشدارهاي مزرعه براي CropLogic هستيد.
قواعد عمومي:
- فقط و فقط JSON معتبر برگردان. هيچ متن اضافه، توضيح، markdown يا code fence توليد نکن.
- لحن حرفه اي، دقيق، کوتاه و اجرايي باشد.
- از اغراق، ترساندن بي دليل و توصيه مبهم خودداري کن.
- اگر داده ناکافي است، اين محدوديت را داخل همان JSON و با متن شفاف بيان کن.
- سطح ها فقط از مقادير مجاز استفاده شوند.
قرارداد خروجي:
1) اگر مسئله مربوط به tracker هشدارها بود، خروجي بايد دقيقا يک آبجکت JSON با اين ساختار باشد:
{
"headline": "جمع بندي کوتاه وضعيت هشدارها",
"overview": "توضيح کوتاه و اجرايي از مهم ترين وضعيت مزرعه",
"status_level": "danger | warning | info",
"notifications": [
{
"level": "danger | warning | info",
"title": "عنوان هشدار",
"message": "شرح کوتاه و روشن هشدار",
"suggested_action": "اقدام پيشنهادي مشخص",
"source_alert_id": "شناسه هشدار يا null",
"source_metric_type": "نوع شاخص يا null"
}
]
}
2) اگر مسئله مربوط به timeline هشدارها بود، خروجي بايد دقيقا يک آبجکت JSON با اين ساختار باشد:
{
"headline": "عنوان کوتاه timeline",
"overview": "شرح کوتاه روند هشدارها",
"timeline": [
{
"timestamp": "ISO timestamp يا null",
"level": "danger | warning | info",
"title": "عنوان رخداد",
"description": "توضيح رخداد و اثر آن",
"source_alert_id": "شناسه هشدار يا null",
"source_metric_type": "نوع شاخص يا null"
}
],
"notifications": [
{
"level": "danger | warning | info",
"title": "عنوان هشدار",
"message": "شرح کوتاه و روشن هشدار",
"suggested_action": "اقدام پيشنهادي مشخص",
"source_alert_id": "شناسه هشدار يا null",
"source_metric_type": "نوع شاخص يا null"
}
]
}
قواعد تکميلي:
- در کانتکست، `incoming_alerts` ممکن است شامل alertها يا سابقه alertهاي چند ساعت تا چند روز گذشته باشد؛ آن ها را بررسي کن و notification تکراري يا هم معنا نساز.
- notification تکراري، هم معنا، يا با اقدام يکسان نسبت به alertهاي موجود در `incoming_alerts` نساز؛ مگر اينکه وضعيت واقعا جديد شده باشد يا شدت هشدار به شکل معنادار تغيير کرده باشد.
- فقط وقتي notification جديد بساز که يک رخداد مهم، جديد، غيرتکراري و قابل اقدام وجود داشته باشد.
- اگر مورد مهم يا جديدي براي گفتن وجود ندارد، `notifications` را خالي برگردان و در `headline` و `overview` صريح و کوتاه بگو که فعلا notification مهم جديدي وجود ندارد.
- اگر alertها خفيف، تکراري يا بدون اقدام تازه هستند، به جاي ساخت notification جديد، همان خروجی بدون notification بده.
- [TEMP_FORCE_MIN_NOTIFICATION_START]
- اين يک قاعده موقت است: اگر هيچ notification مهمي هم وجود نداشت، باز هم حداقل يک notification بساز.
- در اين حالت، يک notification با `level` برابر `info` توليد کن که صريح بگويد فعلا مورد مهم جديدي شناسايي نشده است.
- براي اين notification حداقلي، `title` کوتاه و خنثي باشد، `message` شفاف بگويد هشدار مهم جديدي وجود ندارد، و `suggested_action` فقط يک اقدام پايشي سبک و مشخص باشد.
- اين notification حداقلي فقط وقتي استفاده شود که خروجي در غير اين صورت خالي مي شد.
@@ -0,0 +1,93 @@
شما یک دستیار دقیق برای استخراج برنامه کودهی از متن آزاد کشاورز هستید.
هدف:
- متن آزاد کاربر را به JSON ساختاریافته برنامه کودهی تبدیل کن.
- اگر هر بخش مهمی ناقص بود، به جای حدس زدن سوال بپرس.
قواعد قطعی:
- فقط و فقط JSON معتبر برگردان.
- هیچ متن اضافه، markdown، توضیح بیرون از JSON، کدبلاک یا کلید اضافه تولید نکن.
- اگر حتی یکی از فیلدهای اصلی خالی، null، نامشخص یا مبهم بود باید `status` برابر `needs_clarification` باشد.
- در حالت `completed` هیچ فیلد null یا رشته خالی در `collected_data` و `final_plan` نباید وجود داشته باشد.
- اگر متن ناقص بود، سوال ها باید کوتاه، روشن، غیرتکراری و کاملا کاربردی باشند.
- از حدس زدن نام کود، فرمول، مقدار، روش مصرف، زمان مصرف، فاصله بین نوبت ها یا مرحله رشد خودداری کن.
- اگر کاربر `answers` و `partial_plan` داده باشد، اول آن ها را با متن جدید ادغام کن و فقط سوال های باقی مانده را بپرس.
- اگر چند کود در متن آمده بود، همه را در `applications` لیست کن.
- زبان همه `summary`، `question` و `rationale` ها فارسی باشد.
فیلدهای اصلی که برای تکمیل برنامه لازم هستند:
- `crop_name`
- `growth_stage`
- `fertilizer_name`
- `formula`
- `amount`
- `application_method`
- `timing`
- `interval_days`
ساختار دقیق JSON خروجی:
{
"status": "completed" | "needs_clarification",
"summary": "string",
"missing_fields": ["string"],
"questions": [
{
"id": "string",
"field": "string",
"question": "string",
"rationale": "string"
}
],
"collected_data": {
"crop_name": "string|null",
"growth_stage": "string|null",
"objective": "string|null",
"applications": [
{
"fertilizer_name": "string|null",
"formula": "string|null",
"amount": "string|null",
"application_method": "string|null",
"timing": "string|null",
"interval_days": "integer|null",
"purpose": "string|null"
}
],
"notes": ["string"]
},
"final_plan": {
"crop_name": "string",
"growth_stage": "string",
"objective": "string|null",
"applications": [
{
"fertilizer_name": "string",
"formula": "string",
"amount": "string",
"application_method": "string",
"timing": "string",
"interval_days": "integer",
"purpose": "string|null"
}
],
"notes": ["string"]
} | null
}
منطق وضعیت:
- اگر همه فیلدهای اصلی کامل بودند:
- `status = "completed"`
- `missing_fields = []`
- `questions = []`
- `final_plan` باید کامل و بدون null در فیلدهای اصلی باشد
- اگر حتی یکی از فیلدهای اصلی ناقص بود:
- `status = "needs_clarification"`
- `missing_fields` فقط فیلدهای ناقص را شامل شود
- `questions` برای همان فیلدهای ناقص ساخته شود
- `final_plan = null`
نمونه سوال خوب:
- "محصول الان در چه مرحله رشدی قرار دارد؟"
- "فرمول یا آنالیز کود چیست؟ مثلا 20-20-20."
- "مقدار مصرف هر نوبت کود چقدر است؟"
- "فاصله بین نوبت های مصرف کود چند روز است؟"
@@ -0,0 +1,106 @@
You are the fertilization recommendation assistant for CropLogic.
### GOAL
Use soil data, plant stage, weather risk, retrieved knowledge, and the block named `[خروجي بهينه ساز شبيه سازي]` to produce a farmer-ready Persian fertilization response.
### SOURCE PRIORITY
1. The optimizer block is the source of truth for fertilizer formula, dosage, application method, timing, validity, and scientific priority.
2. Retrieved knowledge can enrich explanations, safety, micro nutrients, and alternative options.
3. Never invent numeric values that conflict with the optimizer block.
### HARD RULES
1. Return only valid JSON. No markdown, no code fences, no greetings.
2. The top-level object must be:
- `status`
- `data`
3. Set `status` to `success` when you can produce the recommendation.
4. Keep all text in clear practical Persian.
5. If some descriptive field is uncertain, keep it short and conservative instead of inventing precise claims.
6. Always include these objects inside `data`:
- `primary_recommendation`
- `nutrient_analysis`
- `application_guide`
- `alternative_recommendations`
### REQUIRED JSON CONTRACT
{
"status": "success",
"data": {
"primary_recommendation": {
"display_title": "عنوان نمايشي کوتاه",
"reasoning": "توضيح علمي و کاربردي بر اساس مرحله رشد، کمبود عناصر، ريسک آب و هوا و شبيه سازي",
"summary": "جمع بندي يک جمله اي مناسب براي Hero Card"
},
"nutrient_analysis": {
"macro": [
{
"key": "n",
"name": "نيتروژن (N)",
"value": 0,
"unit": "percent",
"description": "توضيح کوتاه کاربردي"
},
{
"key": "p",
"name": "فسفر (P)",
"value": 0,
"unit": "percent",
"description": "توضيح کوتاه کاربردي"
},
{
"key": "k",
"name": "پتاسيم (K)",
"value": 0,
"unit": "percent",
"description": "توضيح کوتاه کاربردي"
}
],
"micro": [
{
"key": "zn",
"name": "روي",
"value": 0,
"unit": "percent",
"description": "فقط اگر دانش زمينه اي معتبر داري پر کن"
}
]
},
"application_guide": {
"safety_warning": "هشدار ايمني کوتاه و عملياتي",
"steps": [
{
"step_number": 1,
"title": "آماده سازي",
"description": "شرح کوتاه"
},
{
"step_number": 2,
"title": "اختلاط يا تزريق",
"description": "شرح کوتاه"
},
{
"step_number": 3,
"title": "اجرا و پايش",
"description": "شرح کوتاه"
}
]
},
"alternative_recommendations": [
{
"fertilizer_code": "alt-1",
"fertilizer_name": "نام کود جايگزين",
"fertilizer_type": "NPK يا نوع کاربردي",
"usage_method": "روش مصرف",
"description": "چه زماني و چرا اين جايگزين مفيد است"
}
]
}
}
### WRITING RULES
- Repeat the dominant nutrient gap if the optimizer indicates one.
- If rain, heat, or pH creates a constraint, mention it in `reasoning` and `application_guide.safety_warning`.
- `summary` must be short, direct, and suitable for a hero card.
- `reasoning` must be richer than `summary` and connect simulation plus agronomy.
- `alternative_recommendations` should be concise and realistic; do not add many items.
- `nutrient_analysis.micro` can be an empty array when no trustworthy micronutrient detail exists.
@@ -0,0 +1,87 @@
شما یک دستیار دقیق برای استخراج برنامه آبیاری از متن آزاد کشاورز هستید.
هدف:
- متن آزاد کاربر را به JSON ساختاریافته برنامه آبیاری تبدیل کن.
- اگر هر بخش مهمی ناقص بود، به جای حدس زدن سوال بپرس.
قواعد قطعی:
- فقط و فقط JSON معتبر برگردان.
- هیچ متن اضافه، markdown، توضیح بیرون از JSON، کدبلاک یا کلید اضافه تولید نکن.
- اگر حتی یکی از فیلدهای اصلی خالی، null، نامشخص یا مبهم بود باید `status` برابر `needs_clarification` باشد.
- در حالت `completed` هیچ فیلد null یا رشته خالی در `collected_data` و `final_plan` نباید وجود داشته باشد.
- اگر متن ناقص بود، سوال ها باید کوتاه، روشن، غیرتکراری و کاملا کاربردی باشند.
- از حدس زدن مقدار آب، مدت زمان، فاصله آبیاری، زمان اجرا، مرحله رشد، تاریخ شروع یا محدوده هدف خودداری کن.
- اگر کاربر `answers` و `partial_plan` داده باشد، اول آن ها را با متن جدید ادغام کن و فقط سوال های باقی مانده را بپرس.
- زبان همه `summary`، `question` و `rationale` ها فارسی باشد.
فیلدهای اصلی که برای تکمیل برنامه لازم هستند:
- `crop_name`
- `growth_stage`
- `irrigation_method`
- `water_amount_per_event`
- `duration_minutes`
- `frequency_text`
- `interval_days`
- `preferred_time_of_day`
- `start_date`
- `target_area`
ساختار دقیق JSON خروجی:
{
"status": "completed" | "needs_clarification",
"summary": "string",
"missing_fields": ["string"],
"questions": [
{
"id": "string",
"field": "string",
"question": "string",
"rationale": "string"
}
],
"collected_data": {
"crop_name": "string|null",
"growth_stage": "string|null",
"irrigation_method": "string|null",
"water_amount_per_event": "string|null",
"duration_minutes": "integer|null",
"frequency_text": "string|null",
"interval_days": "integer|null",
"preferred_time_of_day": "string|null",
"start_date": "string|null",
"target_area": "string|null",
"trigger_conditions": ["string"],
"notes": ["string"]
},
"final_plan": {
"crop_name": "string",
"growth_stage": "string",
"irrigation_method": "string",
"water_amount_per_event": "string",
"duration_minutes": "integer",
"frequency_text": "string",
"interval_days": "integer",
"preferred_time_of_day": "string",
"start_date": "string",
"target_area": "string",
"trigger_conditions": ["string"],
"notes": ["string"]
} | null
}
منطق وضعیت:
- اگر همه فیلدهای اصلی کامل بودند:
- `status = "completed"`
- `missing_fields = []`
- `questions = []`
- `final_plan` باید کامل و بدون null باشد
- اگر حتی یکی از فیلدهای اصلی ناقص بود:
- `status = "needs_clarification"`
- `missing_fields` فقط فیلدهای ناقص را شامل شود
- `questions` برای همان فیلدهای ناقص ساخته شود
- `final_plan = null`
نمونه سوال خوب:
- "محصول الان در چه مرحله رشدی قرار دارد؟"
- "این برنامه از چه تاریخی باید شروع شود؟"
- "این برنامه برای کل مزرعه است یا فقط یک بخش خاص؟"
@@ -0,0 +1,75 @@
You are an irrigation recommendation assistant for CropLogic.
### GOAL
Turn the farm context, weather context, FAO-56 calculations, and the block named `[خروجی بهینه ساز شبیه سازی]` into a farmer-friendly Persian JSON response that matches the frontend contract exactly.
### HARD RULES
1. The optimizer block is the source of truth for amount, timing, frequency, validity period, event dates, and stress reasoning. Do not invent conflicting numbers.
2. If both optimizer data and general knowledge are present, prefer optimizer data and use knowledge only to explain why.
3. Always return only valid JSON.
4. The top-level object must contain exactly these keys:
- `plan`
- `water_balance`
- `timeline`
- `sections`
5. Do not return keys such as `raw_response`, `status`, `generated_at`, `recommendation_title`, `recommendation_subtitle`, `final_verdict`, `primary_method`, `usage_summary`, `alternative_plans`, `config`, or `history`.
6. In `sections`, only use `warning` and `tip` as `type`.
7. Write in clear Persian for a farmer. Keep sentences short and practical.
### OUTPUT CONTRACT
{
"plan": {
"frequencyPerWeek": 4,
"durationMinutes": 38,
"bestTimeOfDay": "05:30 تا 08:00 صبح",
"moistureLevel": 72,
"warning": "در ساعات گرم روز آبیاری انجام نشود"
},
"water_balance": {
"active_kc": 0.93,
"crop_profile": {
"kc_initial": 0.55,
"kc_mid": 1.05,
"kc_end": 0.78
},
"daily": [
{
"forecast_date": "2025-02-12",
"et0_mm": 5.4,
"etc_mm": 4.9,
"effective_rainfall_mm": 0,
"gross_irrigation_mm": 17,
"irrigation_timing": "05:30 - 07:00"
}
]
},
"timeline": [
{
"step_number": 1,
"title": "بررسی فشار",
"description": "فشار ابتدا و انتهای لاین کنترل شود"
}
],
"sections": [
{
"title": "هشدار آبیاری",
"icon": "tabler-alert-triangle",
"type": "warning",
"content": "هشدار کوتاه و کاربردی"
},
{
"title": "نکته بهره وری",
"icon": "tabler-bulb",
"type": "tip",
"content": "یک نکته عملی کوتاه"
}
]
}
### WRITING RULES
- `plan.frequencyPerWeek`, `plan.bestTimeOfDay`, and the main warning must align with the optimizer block.
- `water_balance` must be included when FAO-56 or daily balance data is available, preserving the numeric values from the source context.
- `timeline` must be actionable and short. Use 2 to 4 steps when possible.
- If heat stress, rainfall risk, or unusual moisture is present, reflect it in a `warning` section.
- Put maintenance or efficiency advice inside `tip` sections.
- Never output markdown, code fences, greetings, or extra commentary.
@@ -0,0 +1,49 @@
شما دستيار تخصصي آفات و بيماري گياهي براي CropLogic هستيد.
قواعد عمومي:
- فقط و فقط JSON معتبر برگردان. هيچ متن اضافه، markdown يا code fence نده.
- لحن تخصصي، واضح و محتاط باشد.
- از قطعيت کاذب در تشخيص تصويري خودداري کن.
- اگر داده يا شواهد کافي نيست، اين عدم قطعيت را داخل JSON شفاف بيان کن.
- همه متن ها به فارسي و مناسب کاربر مزرعه باشند.
دو نوع خروجي مجاز وجود دارد:
1) اگر مسئله «تشخيص تصويري» بود، فقط اين ساختار JSON را برگردان:
{
"has_issue": true,
"category": "no_issue | pest | disease | nutrient_stress | abiotic_stress | unknown",
"confidence": 0.0,
"severity": "low | medium | high",
"summary": "جمع بندي کوتاه تشخيص",
"detected_signs": ["نشانه 1", "نشانه 2"],
"possible_causes": ["علت 1", "علت 2"],
"immediate_actions": ["اقدام 1", "اقدام 2"],
"reasoning": ["دليل 1", "دليل 2"]
}
2) اگر مسئله «پيش بيني ريسک» بود، فقط اين ساختار JSON را برگردان:
{
"summary": "جمع بندي کوتاه ريسک",
"forecast_window": "بازه زماني",
"overall_risk": "low | medium | high",
"disease_risk": {
"score": 0.0,
"level": "low | medium | high",
"likely_conditions": ["وضعيت 1"],
"reasoning": ["دليل 1", "دليل 2"]
},
"pest_risk": {
"score": 0.0,
"level": "low | medium | high",
"likely_conditions": ["وضعيت 1"],
"reasoning": ["دليل 1", "دليل 2"]
},
"key_drivers": ["عامل 1", "عامل 2"],
"recommended_actions": ["اقدام 1", "اقدام 2"]
}
قواعد تکميلي:
- `confidence` بايد عددي بين 0 و 1 باشد.
- اگر `category` برابر `unknown` يا `no_issue` بود، از توصيه هاي فوري و قطعي پرهيز کن.
- `recommended_actions` و `immediate_actions` بايد عملي، کوتاه و قابل اجرا باشند.
@@ -0,0 +1,22 @@
شما دستيار تخصصي تحليل ناهنجاري داده هاي خاک و سنسور براي CropLogic هستيد.
قواعد عمومي:
- فقط JSON معتبر برگردان. هيچ متن اضافه، markdown يا code fence نده.
- لحن تخصصي، شفاف و محتاط باشد.
- بين «نشانه آماري» و «تشخيص قطعي ميداني» تفاوت بگذار.
- اگر داده کافي نيست، اين محدوديت را داخل JSON صريح بگو.
خروجي بايد دقيقا يک آبجکت JSON با اين ساختار باشد:
{
"summary": "جمع بندي کوتاه ناهنجاري",
"explanation": "توضيح کوتاه از اينکه چه چيزي غيرعادي است",
"likely_cause": "محتمل ترين علت يا علت هاي اصلي",
"recommended_action": "اقدام عملي بعدي",
"monitoring_priority": "low | medium | high | urgent",
"confidence": 0.0
}
قواعد تکميلي:
- `confidence` بايد عددي بين 0 و 1 باشد.
- `recommended_action` بايد عملياتي و کوتاه باشد.
- اگر ناهنجاري معنادار نيست، `summary` و `explanation` بايد اين موضوع را واضح بگويند.
@@ -0,0 +1,21 @@
شما دستيار تخصصي تفسير نياز آبي کوتاه مدت مزرعه براي CropLogic هستيد.
قواعد عمومي:
- فقط JSON معتبر برگردان. هيچ متن اضافه، markdown يا code fence نده.
- عملياتي، دقيق و کوتاه باش.
- اعداد اصلي را فقط از داده ورودي بردار و عدد متناقض جديد نساز.
- اگر forecast يا راندمان آبياري باعث عدم قطعيت مي شود، آن را داخل JSON روشن بگو.
خروجي بايد دقيقا يک آبجکت JSON با اين ساختار باشد:
{
"summary": "جمع بندي نياز آبي بازه کوتاه مدت",
"irrigation_outlook": "برداشت عملياتي از روند آبياري روزهاي آينده",
"recommended_action": "اقدام عملي پيشنهادي براي آبياري",
"risk_note": "ريسک يا عدم قطعيت مهم",
"confidence": 0.0
}
قواعد تکميلي:
- `confidence` بايد عددي بين 0 و 1 باشد.
- `recommended_action` بايد مشخص و قابل اجرا باشد.
- اگر نياز آبي ناچيز است، اين موضوع را مستقيم در `summary` و `irrigation_outlook` بگو.
@@ -0,0 +1,39 @@
You are the narrative assistant for the Yield & Harvest Summary dashboard.
Golden Rule:
- Never generate, infer, estimate, or invent any new numbers, dates, percentages, KPIs, rankings, scores, or comparisons.
- Only use values that already exist in the provided deterministic_data and farm_context.
- If a number, date, or KPI is not present in the input context, do not mention it.
- Do not rewrite a numeric value into a different value, rounded estimate, or alternative unit unless that converted value already exists in the context.
Your job:
- Turn deterministic dashboard data into short, user-friendly text.
- Write subtitles, summaries, descriptions, and operation notes only.
- Keep the wording clear, calm, and practical.
- Preserve the meaning of deterministic blocks exactly.
Output rules:
- Do not add new facts.
- Do not add agronomic claims that are not directly supported by the provided context.
- Do not contradict deterministic_data.
- If the context is incomplete, stay general and say less.
- Prefer concise JSON-ready text fragments over long paragraphs.
Allowed narrative targets:
- season_highlights_card.subtitle
- harvest_prediction_card.description
- harvest_operations_card.summary
- harvest_operations_card.steps[].note
Forbidden behavior:
- No fabricated harvest dates.
- No fabricated yield values.
- No fabricated readiness percentages.
- No fabricated quality grades or market conclusions.
- No speculative recommendations that depend on missing measurements.
Tone:
- Helpful
- Professional
- Simple
- User-facing
+23
View File
@@ -0,0 +1,23 @@
from django.contrib import admin
from django.urls import include, path
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView
urlpatterns = [
path("admin/", admin.site.urls),
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
path("api/docs/swagger/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
path("api/docs/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
# --- App APIs ---
path("api/rag/", include("rag.urls")),
path("api/farm-alerts/", include("farm_alerts.urls")),
path("api/soil-data/", include("location_data.urls")),
path("api/soile/", include("soile.urls")),
path("api/farm-data/", include("farm_data.urls")),
path("api/weather/", include("weather.urls")),
path("api/economy/", include("economy.urls")),
path("api/plants/", include("plant.urls")),
path("api/pest-disease/", include("pest_disease.urls")),
path("api/irrigation/", include("irrigation.urls")),
path("api/fertilization/", include("fertilization.urls")),
path("api/crop-simulation/", include("crop_simulation.urls")),
]
+7
View File
@@ -0,0 +1,7 @@
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_wsgi_application()
+2
View File
@@ -0,0 +1,2 @@
# Keep build-isolated dependency resolution compatible with Python 3.10.
numpy>=1.23,<1.27
@@ -0,0 +1,822 @@
# راهنمای کامل `crop_simulation/services.py`
این فایل توضیح می‌دهد که سرویس‌های شبیه‌سازی در `crop_simulation/services.py` چه کاری انجام می‌دهند، ورودی و خروجی هر بخش چیست، و چگونه با تنظیمات موجود در `irrigation/apps.py` و `fertilization/apps.py` ارتباط می‌گیرند.
---
## نمای کلی
فایل `crop_simulation/services.py` هسته اجرای سناریوهای شبیه‌سازی محصول در پروژه است. این فایل سه مسئولیت اصلی دارد:
1. نرمال‌سازی ورودی‌ها برای موتور شبیه‌سازی
2. اجرای مدل PCSE/WOFOST
3. ذخیره و مدیریت سناریوها و runها در دیتابیس
در عمل این فایل بین داده‌های خام مزرعه/هواشناسی/مدیریتی و خروجی نهایی شبیه‌سازی قرار می‌گیرد.
---
## ساختار کلی فایل
این فایل را می‌توان به ۴ بخش تقسیم کرد:
1. توابع کمکی برای تبدیل ورودی‌ها
2. کلاس `PcseSimulationManager`
3. کلاس `CropSimulationService`
4. wrapperهای سطح ماژول برای استفاده ساده‌تر
---
## بخش اول: ثابت‌ها و Exception
### `DEFAULT_OUTPUT_VARS`
لیست متغیرهایی که از خروجی روزانه مدل می‌خواهیم:
- `DVS`
- `LAI`
- `TAGP`
- `TWSO`
- `SM`
### `DEFAULT_SUMMARY_VARS`
متغیرهای خلاصه:
- `TAGP`
- `TWSO`
- `CTRAT`
- `RD`
### `DEFAULT_TERMINAL_VARS`
متغیرهای انتهایی:
- `TAGP`
- `TWSO`
- `LAI`
- `DVS`
### `CropSimulationError`
خطای اختصاصی این ماژول است. هر جا داده ورودی یا اجرای مدل مشکل داشته باشد، معمولا این exception یا exceptionهای مشتق‌شده از آن دیده می‌شود.
---
## بخش دوم: توابع کمکی داخلی
این توابع public API نیستند، اما پایه رفتار کل سرویس را تشکیل می‌دهند.
### `_json_ready(value)`
داده‌های Python را برای ذخیره در JSON آماده می‌کند.
کارهایی که انجام می‌دهد:
- `dict`، `list` و `tuple` را recursive تبدیل می‌کند
- `date` و `datetime` را به `isoformat()` تبدیل می‌کند
موارد استفاده:
- قبل از ذخیره `input_payload`
- قبل از ذخیره `result_payload`
- قبل از ذخیره payload هر `SimulationRun`
### `_coerce_date(value)`
ورودی را به `date` تبدیل می‌کند.
ورودی قابل قبول:
- `date`
- `datetime`
- رشته ISO مثل `2026-04-01`
اگر نوع پشتیبانی نشود، `CropSimulationError` می‌دهد.
### `_normalize_weather_records(weather)`
ورودی آب‌وهوا را به فرمت استاندارد موردنیاز PCSE تبدیل می‌کند.
ورودی قابل قبول:
- یک `dict`
- یک `list[dict]`
- یک آبجکت با کلید `records`
خروجی همیشه لیستی از رکوردهای نرمال‌شده با کلیدهای زیر است:
- `DAY`
- `LAT`
- `LON`
- `ELEV`
- `IRRAD`
- `TMIN`
- `TMAX`
- `VAP`
- `WIND`
- `RAIN`
- `E0`
- `ES0`
- `ET0`
اگر رکوردها خالی باشند، خطا می‌دهد.
### `_normalize_agromanagement(agromanagement)`
ورودی agromanagement را به یک `list[dict]` تبدیل می‌کند.
ورودی قابل قبول:
- دیکشنری با کلید `AgroManagement`
- لیست
- یک دیکشنری تکی
اگر خالی باشد، خطا می‌دهد.
### `_deep_copy_json_like(value)`
نسخه deep copy ساده از objectهای JSON-like می‌سازد.
برای جلوگیری از mutation روی ورودی اصلی استفاده می‌شود.
### `_parse_recommendation_events(...)`
داده‌های توصیه آبیاری یا کودهی را به فرمت event قابل الحاق به `TimedEvents` تبدیل می‌کند.
این تابع از چند شکل ورودی پشتیبانی می‌کند:
- `events`
- `schedule`
- `applications`
- `plan`
نمونه ورودی آبیاری:
```python
{
"events": [
{"date": "2026-04-25", "amount": 2.5, "efficiency": 0.8}
]
}
```
نمونه خروجی:
```python
[
{
"event_signal": "irrigate",
"name": "irrigate recommendation",
"comment": "",
"events_table": [
{
date(2026, 4, 25): {"amount": 2.5, "efficiency": 0.8}
}
],
}
]
```
### `_merge_management_recommendations(...)`
مهم‌ترین تابع glue برای اتصال recommendationها به شبیه‌سازی است.
کار این تابع:
1. agromanagement را normalize می‌کند
2. توصیه آبیاری را به eventهای `irrigate` تبدیل می‌کند
3. توصیه کودهی را به eventهای `apply_n` تبدیل می‌کند
4. همه آن‌ها را داخل اولین campaign معتبر در `TimedEvents` merge می‌کند
این تابع همان نقطه‌ای است که recommendationهای اپ آبیاری/کودهی به سناریوی شبیه‌سازی تزریق می‌شوند.
### `_normalize_pcse_output_records(records)`
خروجی‌های مدل PCSE را به لیست تبدیل می‌کند تا کدهای بعدی همیشه با ساختار یکنواخت کار کنند.
### `_pick_first_not_none(*values)`
اولین مقدار non-null را برمی‌گرداند.
برای ساخت metricهای نهایی مثل `yield_estimate` استفاده می‌شود.
### `_extract_total_n(agromanagement)`
جمع کل `N_amount` را از eventهای کودهی استخراج می‌کند.
در نسخه فعلی این تابع برای محاسبات جانبی آماده است و نقطه مناسبی برای توسعه تحلیل استراتژی‌های تغذیه است.
### `_load_pcse_bindings()`
کلاس‌ها و ماژول‌های لازم از package `pcse` را load می‌کند:
- `ParameterProvider`
- `WeatherDataProvider`
- `WeatherDataContainer`
- `pcse.models`
اگر `pcse` نصب نباشد، `None` برمی‌گرداند.
### `_resolve_model_class(bindings, model_name)`
کلاس مدل PCSE را با نامی مثل `Wofost81_NWLP_CWB_CNB` پیدا می‌کند.
---
## `PreparedSimulationInput`
این dataclass ورودی‌های نرمال‌شده برای اجرای مدل را نگه می‌دارد:
- `weather`
- `soil`
- `crop`
- `site`
- `agromanagement`
این ساختار باعث می‌شود manager با یک payload استاندارد کار کند.
---
## بخش سوم: `PcseSimulationManager`
این کلاس فقط مسئول اجرای موتور شبیه‌سازی است و وارد منطق ذخیره سناریوها نمی‌شود.
### `__init__(model_name="Wofost81_NWLP_CWB_CNB")`
مدل PCSE مورد استفاده را مشخص می‌کند.
مدل پیش‌فرض:
```python
Wofost81_NWLP_CWB_CNB
```
### `run_simulation(...)`
ورودی خام می‌گیرد، normalize می‌کند، dependencyهای PCSE را load می‌کند، و شبیه‌سازی را اجرا می‌کند.
پارامترها:
- `weather`
- `soil`
- `crop_parameters`
- `agromanagement`
- `site_parameters`
خروجی:
```python
{
"engine": "pcse",
"model_name": "Wofost81_NWLP_CWB_CNB",
"metrics": {...},
"daily_output": [...],
"summary_output": [...],
"terminal_output": [...]
}
```
### `_run_with_pcse(prepared, bindings)`
اجرای واقعی مدل را انجام می‌دهد.
جریان داخلی:
1. ساخت weather provider سفارشی از روی dictها
2. ساخت `ParameterProvider`
3. ساخت instance مدل PCSE
4. اجرای `run_till_terminate()` یا `run()`
5. گرفتن خروجی‌ها
6. تبدیل خروجی به فرم نهایی
### `_build_result(...)`
metricهای کلیدی را از خروجی‌های terminal/summary/daily استخراج می‌کند:
- `yield_estimate`
- `biomass`
- `max_lai`
اولویت انتخاب metricها:
1. terminal
2. summary
3. آخرین رکورد daily
---
## بخش چهارم: `CropSimulationService`
این کلاس service layer سطح بالاتر است. علاوه بر اجرای مدل، سناریوها و runها را در دیتابیس ذخیره می‌کند.
مدل‌های مرتبط:
- `SimulationScenario`
- `SimulationRun`
### `__init__(manager=None)`
اگر manager داده نشود، از `PcseSimulationManager()` پیش‌فرض استفاده می‌شود.
---
## متدهای public اصلی
### 1) `run_single_simulation(...)`
برای اجرای یک سناریوی تکی.
پارامترها:
- `weather`
- `soil`
- `crop_parameters`
- `agromanagement`
- `site_parameters`
- `irrigation_recommendation`
- `fertilization_recommendation`
- `name`
کارها:
1. merge کردن recommendationها داخل management
2. ساخت `SimulationScenario` با نوع `SINGLE`
3. ساخت `SimulationRun`
4. اجرای سناریو
مهم:
اگر recommendationهای آبیاری/کودهی بدهید، این متد آن‌ها را به eventهای مدل تبدیل می‌کند.
نمونه:
```python
from crop_simulation.services import run_single_simulation
result = run_single_simulation(
weather=weather_payload,
soil={"SMFCF": 0.34, "SMW": 0.12, "RDMSOL": 120.0},
crop_parameters={"crop_name": "wheat", "TSUM1": 800},
agromanagement=agromanagement_payload,
site_parameters={"WAV": 40.0},
irrigation_recommendation={
"events": [
{"date": "2026-04-25", "amount": 2.5, "efficiency": 0.8}
]
},
)
```
### 2) `compare_crops(...)`
برای مقایسه دو محصول.
ورودی‌های اضافه:
- `crop_a`
- `crop_b`
خروجی:
- سناریو با نوع `CROP_COMPARISON`
- دو run
- comparison شامل best run و yield gap
### 3) `recommend_best_crop(...)`
برای مقایسه چند محصول و انتخاب بهترین گزینه.
ورودی مهم:
- `crops: list[dict]`
شرط:
- حداقل دو crop باید وجود داشته باشد
خروجی ساده‌شده:
```python
{
"scenario_id": ...,
"scenario_type": "crop_comparison",
"recommended_crop": {
"run_key": "...",
"label": "...",
"expected_yield_estimate": ...
},
"candidates": [...],
"raw_result": {...}
}
```
### 4) `compare_fertilization_strategies(...)`
برای مقایسه چند strategy کودهی روی یک crop ثابت.
ورودی ویژه:
```python
strategies = [
{
"label": "base",
"agromanagement": [...]
},
{
"label": "high_n",
"agromanagement": [...]
}
]
```
این متد برای هر strategy یک run می‌سازد و بهترین استراتژی را بر اساس `yield_estimate` انتخاب می‌کند.
### 5) `get_scenario_result(scenario_id)`
نتیجه ذخیره‌شده یک سناریو را از دیتابیس برمی‌گرداند.
خروجی شامل:
- اطلاعات scenario
- اطلاعات همه runها
- status
- input payload
- result payload
- error message
---
## متدهای داخلی مهم در `CropSimulationService`
### `_execute_scenario(...)`
قلب اجرای سناریو است.
جریان:
1. status سناریو را `RUNNING` می‌کند
2. تک‌تک runها را اجرا می‌کند
3. خروجی هر run را ذخیره می‌کند
4. اگر exception رخ دهد:
- همان run را `FAILURE` می‌کند
- سناریو را `FAILURE` می‌کند
- خطا را ذخیره می‌کند
5. اگر همه چیز موفق باشد:
- `scenario_result` می‌سازد
- سناریو را `SUCCESS` می‌کند
### `_build_scenario_result(scenario, results)`
خروجی سطح سناریو را می‌سازد.
رفتار بر اساس نوع سناریو:
- `SINGLE`:
- فقط `result` برمی‌گرداند
- `CROP_COMPARISON`:
- comparison می‌سازد
- بهترین run را مشخص می‌کند
- `yield_gap` می‌سازد
- `FERTILIZATION_COMPARISON`:
- recommendation برای بهترین strategy می‌سازد
---
## wrapperهای سطح ماژول
در انتهای فایل این wrapperها وجود دارند:
- `run_single_simulation(**kwargs)`
- `compare_crops(**kwargs)`
- `recommend_best_crop(**kwargs)`
- `compare_fertilization_strategies(**kwargs)`
همه آن‌ها با `@transaction.atomic` تزئین شده‌اند.
یعنی اگر بخواهید ساده از بیرون صدا بزنید، لازم نیست خودتان instance بسازید:
```python
from crop_simulation.services import recommend_best_crop
result = recommend_best_crop(
weather=weather_payload,
soil=soil_payload,
crops=[crop_a, crop_b, crop_c],
agromanagement=agromanagement_payload,
)
```
---
## نحوه ارتباط با مدل‌های دیتابیس
### `SimulationScenario`
نماینده یک سناریوی کلی است.
مثال‌ها:
- single run
- crop comparison
- fertilization comparison
### `SimulationRun`
نماینده هر اجرای منفرد داخل یک سناریو است.
مثلا در `compare_crops`:
- یک `SimulationScenario`
- دو `SimulationRun`
---
## ارتباط `crop_simulation/services.py` با `crop_simulation/apps.py`
فایل `crop_simulation/apps.py` این متد را expose می‌کند:
```python
def get_recommendation_optimizer(self):
return self.recommendation_optimizer
```
این optimizer در فایل `crop_simulation/recommendation_optimizer.py` ساخته می‌شود و برای recommendationهای آبیاری و کودهی استفاده می‌شود.
نکته مهم:
- `services.py` موتور اجرای سناریوهاست
- `recommendation_optimizer.py` روی همین موتور سناریوهای candidate می‌سازد
- `apps.py` فقط نقطه دسترسی مرکزی به optimizer است
یعنی:
```python
optimizer = apps.get_app_config("crop_simulation").get_recommendation_optimizer()
```
و بعد optimizer در داخل خودش از `CropSimulationService` استفاده می‌کند.
---
## ارتباط با `irrigation/apps.py`
فایل `irrigation/apps.py` خودش شبیه‌سازی اجرا نمی‌کند؛ بلکه تنظیمات default برای optimizer آبیاری را نگه می‌دارد.
### فیلدهای مهم
#### `tone_file`
مسیر tone مربوط به LLM:
```python
config/tones/irrigation_tone.txt
```
#### `optimizer_defaults`
این property تنظیمات پایه بهینه‌سازی آبیاری را برمی‌گرداند:
- `validity_days`
- `minimum_event_mm`
- `significant_rain_threshold_mm`
- `stage_targets`
- `strategy_profiles`
### `stage_targets`
هدف رطوبت یا رفتار پایه برای stageهای مختلف:
- `initial`
- `vegetative`
- `flowering`
- `fruiting`
### `strategy_profiles`
سه سناریوی پایه برای optimizer:
- `conservative`
- `balanced`
- `protective`
هر سناریو مشخص می‌کند:
- ضریب آب (`multiplier`)
- ضریب تعداد دفعات (`frequency_factor`)
- تعداد event پایه (`event_count`)
### نحوه استفاده در کد
در optimizer آبیاری معمولا به شکل زیر خوانده می‌شود:
```python
defaults = apps.get_app_config("irrigation").get_optimizer_defaults()
```
سپس این defaults به سناریوهای recommendation تبدیل می‌شوند و در صورت نیاز به `run_single_simulation()` پاس داده می‌شوند.
### نقش آن در ارتباط با `services.py`
ارتباط غیرمستقیم است:
1. `irrigation/apps.py` تنظیمات baseline را می‌دهد
2. optimizer با این تنظیمات candidate strategy می‌سازد
3. strategyها به recommendation event تبدیل می‌شوند
4. `crop_simulation/services.py` آن‌ها را داخل agromanagement merge و اجرا می‌کند
---
## ارتباط با `fertilization/apps.py`
این فایل مشابه irrigation است اما برای منطق کودهی.
### `tone_file`
```python
config/tones/fertilization_tone.txt
```
### `optimizer_defaults`
این تنظیمات را می‌دهد:
- `validity_days`
- `rain_delay_threshold_mm`
- `stage_targets`
- `strategy_profiles`
### `stage_targets`
برای هر stage اطلاعات زیر مشخص می‌شود:
- `n`
- `p`
- `k`
- `formula`
- `application_method`
- `timing`
### `strategy_profiles`
سناریوهای پایه:
- `maintenance`
- `balanced`
- `corrective`
هرکدام مشخص می‌کنند:
- ضریب مصرف (`multiplier`)
- focus تغذیه‌ای
- روش مصرف
- override فرمول در صورت نیاز
### نحوه استفاده در کد
```python
defaults = apps.get_app_config("fertilization").get_optimizer_defaults()
```
سپس optimizer با این defaults چند strategy می‌سازد. اگر لازم باشد این strategyها به `compare_fertilization_strategies()` یا `run_single_simulation()` داده می‌شوند.
### ارتباط آن با `services.py`
ارتباط باز هم غیرمستقیم است:
1. `fertilization/apps.py` پروفایل stage و strategy را می‌دهد
2. optimizer از روی آن strategy تولید می‌کند
3. strategy به eventهای `apply_n` تبدیل می‌شود
4. `services.py` این eventها را داخل agromanagement merge می‌کند
5. سناریو اجرا و مقایسه می‌شود
---
## الگوی ارتباط کامل بین سه بخش
### سناریوی آبیاری
```text
irrigation/apps.py
-> optimizer_defaults
-> recommendation optimizer
-> irrigation recommendation events
-> crop_simulation/services.py:_merge_management_recommendations()
-> run_single_simulation()
-> PCSE run
-> scenario/run result
```
### سناریوی کودهی
```text
fertilization/apps.py
-> optimizer_defaults
-> recommendation optimizer
-> fertilization recommendation events
-> crop_simulation/services.py:_merge_management_recommendations()
-> compare_fertilization_strategies() / run_single_simulation()
-> PCSE run
-> best strategy result
```
---
## نمونه استفاده واقعی
### اجرای یک شبیه‌سازی ساده
```python
from crop_simulation.services import run_single_simulation
result = run_single_simulation(
weather=[
{
"DAY": "2026-04-01",
"LAT": 35.7,
"LON": 51.4,
"ELEV": 1200,
"IRRAD": 16000000,
"TMIN": 11,
"TMAX": 22,
"VAP": 12,
"WIND": 2.4,
"RAIN": 0.8,
"E0": 0.35,
"ES0": 0.3,
"ET0": 0.32,
}
],
soil={"SMFCF": 0.34, "SMW": 0.12, "RDMSOL": 120.0},
crop_parameters={"crop_name": "wheat", "TSUM1": 800, "YIELD_SCALE": 1.0},
agromanagement=[
{
"2026-04-01": {
"CropCalendar": {
"crop_name": "wheat",
"variety_name": "winter-wheat",
"crop_start_date": "2026-04-05",
"crop_start_type": "sowing",
"crop_end_date": "2026-09-01",
"crop_end_type": "harvest",
"max_duration": 180,
},
"TimedEvents": [],
"StateEvents": [],
}
}
],
site_parameters={"WAV": 40.0},
)
```
### مقایسه دو محصول
```python
from crop_simulation.services import compare_crops
result = compare_crops(
weather=weather_payload,
soil=soil_payload,
crop_a={"crop_name": "wheat", "TSUM1": 800},
crop_b={"crop_name": "maize", "TSUM1": 900},
agromanagement=agromanagement_payload,
site_parameters={"WAV": 40.0},
)
```
### مقایسه strategyهای کودهی
```python
from crop_simulation.services import compare_fertilization_strategies
result = compare_fertilization_strategies(
weather=weather_payload,
soil=soil_payload,
crop_parameters={"crop_name": "wheat", "TSUM1": 800},
strategies=[
{"label": "base", "agromanagement": agm_base},
{"label": "high_n", "agromanagement": agm_high_n},
],
site_parameters={"WAV": 40.0},
)
```
---
## نکات مهم توسعه
### 1. نقطه اصلی inject کردن توصیه‌ها
اگر بخواهید recommendationهای جدید را وارد شبیه‌سازی کنید، مهم‌ترین نقطه:
```python
_merge_management_recommendations()
```
### 2. نقطه اصلی اجرای موتور
اگر بخواهید backend engine عوض شود یا مدل جدید اضافه شود:
```python
PcseSimulationManager.run_simulation()
```
### 3. نقطه اصلی مدیریت lifecycle سناریو
اگر بخواهید queueing، logging یا audit بیشتری اضافه کنید:
```python
CropSimulationService._execute_scenario()
```
### 4. ارتباط با اپ‌های recommendation
اگر stageها یا strategyهای آبیاری/کودهی تغییر کنند، باید این فایل‌ها بررسی شوند:
- `irrigation/apps.py`
- `fertilization/apps.py`
چون optimizer از آن‌ها defaultها را می‌خواند.
---
## جمع‌بندی
اگر بخواهیم نقش هر فایل را در یک جمله بگوییم:
- `crop_simulation/services.py`: اجرای شبیه‌سازی، ساخت scenario/run، و merge کردن recommendationها با management
- `crop_simulation/apps.py`: نقطه دسترسی مرکزی به optimizer
- `irrigation/apps.py`: تنظیمات پایه برای سناریوهای بهینه‌سازی آبیاری
- `fertilization/apps.py`: تنظیمات پایه برای سناریوهای بهینه‌سازی کودهی
و زنجیره کلی این است:
```text
defaults in app config
-> optimizer
-> recommendation events
-> crop_simulation/services.py
-> PCSE execution
-> scenario result
```
اگر بخواهید، قدم بعدی می‌توانم یک فایل دوم هم بسازم که فقط نمونه request/response واقعی برای هر تابع و هر سناریو را به‌صورت cookbook نشان بدهد.
+1
View File
@@ -0,0 +1 @@
+54
View File
@@ -0,0 +1,54 @@
from functools import cached_property
from django.apps import AppConfig
class CropSimulationConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "crop_simulation"
verbose_name = "Crop Simulation"
@cached_property
def recommendation_optimizer(self):
from .recommendation_optimizer import SimulationRecommendationOptimizer
return SimulationRecommendationOptimizer()
@cached_property
def current_farm_chart_simulator(self):
from .growth_simulation import CurrentFarmChartSimulator
return CurrentFarmChartSimulator()
@cached_property
def harvest_prediction_service(self):
from .harvest_prediction import HarvestPredictionService
return HarvestPredictionService()
@cached_property
def yield_prediction_service(self):
from .yield_prediction import YieldPredictionService
return YieldPredictionService()
@cached_property
def water_stress_service(self):
from .water_stress import WaterStressSimulationService
return WaterStressSimulationService()
def get_recommendation_optimizer(self):
return self.recommendation_optimizer
def get_current_farm_chart_simulator(self):
return self.current_farm_chart_simulator
def get_harvest_prediction_service(self):
return self.harvest_prediction_service
def get_yield_prediction_service(self):
return self.yield_prediction_service
def get_water_stress_service(self):
return self.water_stress_service
@@ -0,0 +1,802 @@
from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass
from datetime import date, datetime, timedelta
from math import exp
from typing import Any
import logging
from django.core.paginator import EmptyPage, Paginator
from farm_data.models import SensorData
from farm_data.services import get_canonical_farm_record, get_runtime_plant_for_farm
from location_data.satellite_snapshot import build_location_satellite_snapshot
from plant.gdd import calculate_daily_gdd, resolve_growth_profile
from weather.models import WeatherForecast
from .services import CropSimulationService, build_simulation_payload_from_farm
DEFAULT_DYNAMIC_PARAMETERS = ["DVS", "LAI", "TAGP", "TWSO", "SM"]
DEFAULT_PAGE_SIZE = 10
MAX_PAGE_SIZE = 50
logger = logging.getLogger(__name__)
DEFAULT_STAGE_LABELS = {
"pre_emergence": "پیش از سبز شدن",
"establishment": "استقرار",
"vegetative": "رشد رویشی",
"flowering": "گلدهی",
"reproductive": "پرشدن محصول",
"maturity": "رسیدگی",
}
ENGINE_LABELS = {
"pcse": "موتور شبیه سازی PCSE",
"growth_projection": "موتور برآورد رشد",
}
MODEL_LABELS = {
"growth_projection_v1": "مدل برآورد رشد نسخه ۱",
"wofost": "مدل ووفوست",
}
class GrowthSimulationError(Exception):
pass
def _fa_engine_name(name: str | None) -> str | None:
if not name:
return name
return ENGINE_LABELS.get(name, name)
def _fa_model_name(name: str | None) -> str | None:
if not name:
return name
return MODEL_LABELS.get(name, name)
@dataclass
class GrowthSimulationContext:
farm_uuid: str | None
plant_name: str
plant: Any
dynamic_parameters: list[str]
weather: list[dict[str, Any]]
crop_parameters: dict[str, Any]
soil_parameters: dict[str, Any]
site_parameters: dict[str, Any]
agromanagement: list[dict[str, Any]]
page_size: int
def _safe_float(value: Any, default: float = 0.0) -> float:
try:
if value in (None, ""):
return default
return float(value)
except (TypeError, ValueError):
return default
def _pick_first_not_none(*values: Any) -> Any:
for value in values:
if value is not None:
return value
return None
def _clamp(value: float, minimum: float, maximum: float) -> float:
if minimum > maximum:
minimum, maximum = maximum, minimum
return max(minimum, min(value, maximum))
def _mm_to_cm_day(value: Any, default: float) -> float:
scaled = _safe_float(value, default * 10.0) / 10.0
return round(max(scaled, 0.0), 4)
def _coerce_date(value: Any) -> date:
if isinstance(value, date) and not isinstance(value, datetime):
return value
if isinstance(value, datetime):
return value.date()
if isinstance(value, str):
return date.fromisoformat(value)
raise GrowthSimulationError(f"Invalid date value: {value!r}")
def _json_ready(value: Any) -> Any:
if isinstance(value, dict):
return {str(key): _json_ready(item) for key, item in value.items()}
if isinstance(value, list):
return [_json_ready(item) for item in value]
if isinstance(value, tuple):
return [_json_ready(item) for item in value]
if isinstance(value, (date, datetime)):
return value.isoformat()
return value
def _normalize_weather_records(weather: Any) -> list[dict[str, Any]]:
if not weather:
return []
records = weather.get("records") if isinstance(weather, dict) and "records" in weather else weather
if not isinstance(records, list):
records = [records]
normalized = []
for item in records:
if not isinstance(item, dict):
raise GrowthSimulationError("Weather records must be JSON objects.")
current_date = _coerce_date(item.get("DAY") or item.get("day"))
normalized.append(
{
"DAY": current_date,
"LAT": _safe_float(item.get("LAT", item.get("lat")), 35.7),
"LON": _safe_float(item.get("LON", item.get("lon")), 51.4),
"ELEV": _safe_float(item.get("ELEV", item.get("elev")), 1200.0),
"IRRAD": _safe_float(item.get("IRRAD", item.get("irrad")), 16_000_000.0),
"TMIN": _safe_float(item.get("TMIN", item.get("tmin")), 12.0),
"TMAX": _safe_float(item.get("TMAX", item.get("tmax")), 24.0),
"VAP": _safe_float(item.get("VAP", item.get("vap")), 12.0),
"WIND": _safe_float(item.get("WIND", item.get("wind")), 2.0),
"RAIN": _safe_float(item.get("RAIN", item.get("rain")), 0.0),
"E0": _safe_float(item.get("E0", item.get("e0")), 0.35),
"ES0": _safe_float(item.get("ES0", item.get("es0")), 0.3),
"ET0": _safe_float(item.get("ET0", item.get("et0")), 0.32),
}
)
if not normalized:
raise GrowthSimulationError("At least one weather record is required.")
return normalized
def _build_weather_from_farm(sensor: SensorData) -> list[dict[str, Any]]:
forecasts = list(
WeatherForecast.objects.filter(location=sensor.center_location)
.order_by("forecast_date")[:14]
)
if not forecasts:
raise GrowthSimulationError("No forecast data found for the selected farm.")
records = []
for forecast in forecasts:
records.append(
{
"DAY": forecast.forecast_date,
"LAT": float(sensor.center_location.latitude),
"LON": float(sensor.center_location.longitude),
"ELEV": 1200.0,
"IRRAD": 16_000_000.0,
"TMIN": _safe_float(forecast.temperature_min, 12.0),
"TMAX": _safe_float(forecast.temperature_max, 24.0),
"VAP": max(_safe_float(forecast.humidity_mean, 55.0) / 5.0, 6.0),
"WIND": _safe_float(forecast.wind_speed_max, 7.2) / 3.6,
# WeatherForecast stores precipitation/ET0 in mm/day, while PCSE expects cm/day.
"RAIN": _mm_to_cm_day(forecast.precipitation, 0.0),
"E0": _mm_to_cm_day(forecast.et0, 0.35),
"ES0": max(round(_mm_to_cm_day(forecast.et0, 0.35) * 0.9, 4), 0.1),
"ET0": _mm_to_cm_day(forecast.et0, 0.35),
}
)
return records
def _build_soil_and_site_from_farm(sensor: SensorData) -> tuple[dict[str, Any], dict[str, Any]]:
satellite_metrics = build_location_satellite_snapshot(sensor.center_location).get("resolved_metrics") or {}
ndwi = _safe_float(satellite_metrics.get("ndwi"), 0.28)
smfcf = _safe_float(ndwi, 0.34)
smw = max(round(smfcf * 0.45, 3), 0.12)
sm0 = min(max(smfcf + 0.08, smw + 0.12), 0.6)
soil_moisture = None
payload = sensor.sensor_payload or {}
if isinstance(payload, dict):
for block in payload.values():
if isinstance(block, dict) and block.get("soil_moisture") is not None:
soil_moisture = _safe_float(block.get("soil_moisture"))
break
site = {
"WAV": soil_moisture if soil_moisture is not None else 40.0,
"IFUNRN": 0,
"NOTINF": 0.0,
"SSI": 0.0,
"SSMAX": 0.0,
"SMLIM": round(_clamp(smfcf, smw, sm0), 3),
}
soil = {
"SMFCF": smfcf,
"SMW": smw,
"SM0": sm0,
"RDMSOL": 120.0,
"CRAIRC": 0.06,
"SOPE": 10.0,
"KSUB": 10.0,
}
return soil, site
def _build_default_crop_parameters(plant: Any) -> dict[str, Any]:
profile = resolve_growth_profile(plant)
required_gdd = _safe_float(profile.get("required_gdd_for_maturity"), 1200.0)
return {
"crop_name": plant.name,
"TSUM1": round(required_gdd * 0.45, 3),
"TSUM2": round(required_gdd * 0.55, 3),
"YIELD_SCALE": 1.0,
"MAX_LAI": 5.0,
"MAX_BIOMASS": 12000.0,
}
def _build_default_agromanagement(plant_name: str, weather: list[dict[str, Any]]) -> list[dict[str, Any]]:
first_day = weather[0]["DAY"]
last_day = weather[-1]["DAY"]
crop_start = first_day
crop_end = max(last_day, crop_start + timedelta(days=1))
return [
{
first_day: {
"CropCalendar": {
"crop_name": plant_name,
"variety_name": "default",
"crop_start_date": crop_start,
"crop_start_type": "sowing",
"crop_end_date": crop_end,
"crop_end_type": "harvest",
"max_duration": max((crop_end - crop_start).days, 1),
},
"TimedEvents": [],
"StateEvents": [],
}
},
{},
]
def _resolve_plant_simulation_defaults(plant: Any) -> tuple[dict[str, Any] | None, list[dict[str, Any]] | None]:
for attr in ("growth_profile", "irrigation_profile", "health_profile"):
profile = getattr(plant, attr, None) or {}
if not isinstance(profile, dict):
continue
simulation = profile.get("simulation")
if not isinstance(simulation, dict):
continue
crop_parameters = simulation.get("crop_parameters")
agromanagement = simulation.get("agromanagement")
if isinstance(crop_parameters, dict) and agromanagement:
return deepcopy(crop_parameters), deepcopy(agromanagement)
return None, None
def build_growth_context(payload: dict[str, Any]) -> GrowthSimulationContext:
plant_name = payload["plant_name"]
from plant.models import Plant
plant = Plant.objects.filter(name=plant_name).first()
if plant is None:
raise GrowthSimulationError("Plant not found.")
dynamic_parameters = payload.get("dynamic_parameters") or DEFAULT_DYNAMIC_PARAMETERS
page_size = min(max(int(payload.get("page_size") or DEFAULT_PAGE_SIZE), 1), MAX_PAGE_SIZE)
sensor = None
resolved_farm_uuid = str(payload["farm_uuid"]) if payload.get("farm_uuid") else None
if payload.get("farm_uuid"):
sensor = (
SensorData.objects.select_related("center_location")
.filter(farm_uuid=payload["farm_uuid"])
.first()
)
if sensor is None:
raise GrowthSimulationError("Farm not found.")
if resolved_farm_uuid:
farm_payload = build_simulation_payload_from_farm(
farm_uuid=resolved_farm_uuid,
plant_name=plant_name,
weather=payload.get("weather"),
soil=payload.get("soil_parameters"),
crop_parameters=payload.get("crop_parameters"),
agromanagement=payload.get("agromanagement"),
site_parameters=payload.get("site_parameters"),
)
weather = farm_payload["weather"]
crop_parameters = farm_payload["crop_parameters"]
soil_parameters = farm_payload["soil"]
site_parameters = farm_payload["site_parameters"]
agromanagement = farm_payload["agromanagement"]
plant = farm_payload["plant"] or plant
return GrowthSimulationContext(
farm_uuid=resolved_farm_uuid,
plant_name=plant_name,
plant=plant,
dynamic_parameters=dynamic_parameters,
weather=weather,
crop_parameters=crop_parameters,
soil_parameters=soil_parameters,
site_parameters=site_parameters,
agromanagement=agromanagement,
page_size=page_size,
)
weather = (
_normalize_weather_records(payload["weather"])
if payload.get("weather")
else _build_weather_from_farm(sensor)
if sensor is not None
else []
)
if not weather:
raise GrowthSimulationError("Weather input is required.")
default_crop_parameters, default_agromanagement = _resolve_plant_simulation_defaults(plant)
crop_parameters = deepcopy(payload.get("crop_parameters") or default_crop_parameters or _build_default_crop_parameters(plant))
crop_parameters.setdefault("crop_name", plant.name)
soil_parameters = deepcopy(payload.get("soil_parameters") or {})
site_parameters = deepcopy(payload.get("site_parameters") or {})
if sensor is not None:
farm_soil, farm_site = _build_soil_and_site_from_farm(sensor)
soil_parameters = {**farm_soil, **soil_parameters}
site_parameters = {**farm_site, **site_parameters}
soil_parameters.setdefault("SMFCF", 0.34)
soil_parameters.setdefault("SMW", 0.14)
soil_parameters.setdefault("SM0", 0.42)
soil_parameters.setdefault("RDMSOL", 120.0)
soil_parameters.setdefault("CRAIRC", 0.06)
soil_parameters.setdefault("SOPE", 10.0)
soil_parameters.setdefault("KSUB", 10.0)
site_parameters.setdefault("WAV", 40.0)
site_parameters.setdefault("IFUNRN", 0)
site_parameters.setdefault("NOTINF", 0.0)
site_parameters.setdefault("SSI", 0.0)
site_parameters.setdefault("SSMAX", 0.0)
site_parameters.setdefault(
"SMLIM",
round(
_clamp(
_safe_float(site_parameters.get("SMLIM"), soil_parameters.get("SMFCF", 0.34)),
_safe_float(soil_parameters.get("SMW"), 0.14),
_safe_float(soil_parameters.get("SM0"), 0.42),
),
3,
),
)
agromanagement = deepcopy(
payload.get("agromanagement")
or default_agromanagement
or _build_default_agromanagement(plant.name, weather)
)
return GrowthSimulationContext(
farm_uuid=resolved_farm_uuid,
plant_name=plant_name,
plant=plant,
dynamic_parameters=dynamic_parameters,
weather=weather,
crop_parameters=crop_parameters,
soil_parameters=soil_parameters,
site_parameters=site_parameters,
agromanagement=agromanagement,
page_size=page_size,
)
def _derive_stage(dvs: float) -> tuple[str, str]:
if dvs < 0:
return "pre_emergence", DEFAULT_STAGE_LABELS["pre_emergence"]
if dvs < 0.2:
return "establishment", DEFAULT_STAGE_LABELS["establishment"]
if dvs < 1.0:
return "vegetative", DEFAULT_STAGE_LABELS["vegetative"]
if dvs < 1.3:
return "flowering", DEFAULT_STAGE_LABELS["flowering"]
if dvs < 2.0:
return "reproductive", DEFAULT_STAGE_LABELS["reproductive"]
return "maturity", DEFAULT_STAGE_LABELS["maturity"]
def _logistic(value: float, midpoint: float, steepness: float, upper: float) -> float:
return upper / (1.0 + exp(-steepness * (value - midpoint)))
def _run_projection_engine(context: GrowthSimulationContext) -> dict[str, Any]:
profile = resolve_growth_profile(context.plant)
required_gdd = _safe_float(profile.get("required_gdd_for_maturity"), 1200.0)
current_gdd = _safe_float(profile.get("current_cumulative_gdd"), 0.0)
base_temperature = _safe_float(profile.get("base_temperature"), 10.0)
max_lai = _safe_float(context.crop_parameters.get("MAX_LAI"), 5.0)
max_biomass = _safe_float(context.crop_parameters.get("MAX_BIOMASS"), 12000.0)
soil_moisture = _safe_float(context.site_parameters.get("WAV"), 40.0)
daily_output = []
for record in context.weather:
tmax = _safe_float(record.get("TMAX"), 24.0)
tmin = _safe_float(record.get("TMIN"), 12.0)
rain = _safe_float(record.get("RAIN"), 0.0)
et0 = _safe_float(record.get("ET0"), 0.32)
daily_gdd = calculate_daily_gdd(tmax=tmax, tmin=tmin, tbase=base_temperature)
current_gdd += daily_gdd
dvs = min(max((current_gdd / max(required_gdd, 1.0)) * 2.0, 0.0), 2.0)
if dvs <= 1.0:
lai = _logistic(dvs, midpoint=0.55, steepness=7.5, upper=max_lai)
else:
decline_factor = max(0.25, 1.0 - ((dvs - 1.0) / 1.1))
lai = max_lai * decline_factor
biomass_factor = min(current_gdd / max(required_gdd, 1.0), 1.25)
weather_modifier = max(0.65, min(1.15, 1.0 + (rain * 0.02) - (et0 * 0.08)))
tagp = max_biomass * biomass_factor * weather_modifier
twso = tagp * max(min((dvs - 0.95) / 0.85, 0.55), 0.0)
soil_moisture = max(5.0, min(95.0, soil_moisture + (rain * 0.7) - (et0 * 8.5)))
entry = {
"DAY": record["DAY"],
"DVS": round(dvs, 4),
"LAI": round(lai, 4),
"TAGP": round(tagp, 4),
"TWSO": round(twso, 4),
"SM": round(soil_moisture / 100.0, 4),
"GDD": round(daily_gdd, 4),
"TMIN": round(tmin, 4),
"TMAX": round(tmax, 4),
"RAIN": round(rain, 4),
"ET0": round(et0, 4),
}
daily_output.append(entry)
final_entry = daily_output[-1] if daily_output else {}
return {
"engine": "growth_projection",
"model_name": "growth_projection_v1",
"metrics": {
"yield_estimate": round(_safe_float(final_entry.get("TWSO"), 0.0), 4),
"biomass": round(_safe_float(final_entry.get("TAGP"), 0.0), 4),
"max_lai": round(max((_safe_float(item.get("LAI"), 0.0) for item in daily_output), default=0.0), 4),
},
"daily_output": _json_ready(daily_output),
"summary_output": [],
"terminal_output": [_json_ready(final_entry)] if final_entry else [],
}
def _run_simulation(
context: GrowthSimulationContext,
*,
irrigation_recommendation: dict[str, Any] | None = None,
fertilization_recommendation: dict[str, Any] | None = None,
) -> tuple[dict[str, Any], int | None, str | None]:
try:
response = CropSimulationService().run_single_simulation(
farm_uuid=context.farm_uuid,
plant_name=context.plant_name,
weather=context.weather,
soil=context.soil_parameters,
crop_parameters=context.crop_parameters,
agromanagement=context.agromanagement,
site_parameters=context.site_parameters,
irrigation_recommendation=irrigation_recommendation,
fertilization_recommendation=fertilization_recommendation,
name=f"growth:{context.plant_name}",
)
return response["result"], response.get("scenario_id"), None
except Exception as exc:
logger.warning(
"Falling back to projection engine for farm_uuid=%s plant_name=%s because PCSE failed: %s",
context.farm_uuid,
context.plant_name,
exc,
)
fallback_result = _run_projection_engine(context)
warning = f"موتور شبیه سازی با خطا مواجه شد و برآورد جایگزین استفاده شد: {exc}"
return fallback_result, None, warning
def summarize_growth_stages(
daily_output: list[dict[str, Any]],
dynamic_parameters: list[str],
) -> list[dict[str, Any]]:
if not daily_output:
return []
stage_items = []
current = None
for raw in daily_output:
record = dict(raw)
day = _coerce_date(record.get("DAY") or record.get("day"))
dvs = _safe_float(record.get("DVS"), 0.0)
stage_code, stage_name = _derive_stage(dvs)
parameter_values = {}
for param in dynamic_parameters:
if record.get(param) is not None:
parameter_values[param] = _safe_float(record.get(param))
if current is None or current["stage_code"] != stage_code:
if current is not None:
stage_items.append(current)
current = {
"stage_code": stage_code,
"stage_name": stage_name,
"start_date": day,
"end_date": day,
"days_count": 1,
"raw_days": [
{
"date": day,
"parameters": parameter_values,
}
],
}
continue
current["end_date"] = day
current["days_count"] += 1
current["raw_days"].append({"date": day, "parameters": parameter_values})
if current is not None:
stage_items.append(current)
summarized = []
for index, item in enumerate(stage_items, start=1):
metrics = {}
for param in dynamic_parameters:
values = [
day_item["parameters"][param]
for day_item in item["raw_days"]
if param in day_item["parameters"]
]
if not values:
continue
metrics[param] = {
"start": round(values[0], 4),
"end": round(values[-1], 4),
"min": round(min(values), 4),
"max": round(max(values), 4),
"avg": round(sum(values) / len(values), 4),
}
summarized.append(
{
"order": index,
"stage_code": item["stage_code"],
"stage_name": item["stage_name"],
"start_date": item["start_date"].isoformat(),
"end_date": item["end_date"].isoformat(),
"days_count": item["days_count"],
"metrics": metrics,
}
)
return summarized
def paginate_growth_stages(
stage_timeline: list[dict[str, Any]],
*,
page: int,
page_size: int,
) -> dict[str, Any]:
page_size = min(max(page_size, 1), MAX_PAGE_SIZE)
if not stage_timeline:
return {
"items": [],
"pagination": {
"page": 1,
"page_size": page_size,
"total_items": 0,
"total_pages": 0,
"has_next": False,
"has_previous": False,
},
}
paginator = Paginator(stage_timeline, page_size)
try:
page_obj = paginator.page(page)
except EmptyPage:
page_obj = paginator.page(paginator.num_pages or 1)
return {
"items": list(page_obj.object_list),
"pagination": {
"page": page_obj.number,
"page_size": page_size,
"total_items": paginator.count,
"total_pages": paginator.num_pages,
"has_next": page_obj.has_next(),
"has_previous": page_obj.has_previous(),
},
}
def run_growth_simulation(payload: dict[str, Any], progress_callback=None) -> dict[str, Any]:
context = build_growth_context(payload)
if progress_callback is not None:
progress_callback(
state="PROGRESS",
meta={"current": 1, "total": 3, "message": "simulation input resolved"},
)
simulation_result, scenario_id, simulation_error = _run_simulation(
context,
irrigation_recommendation=payload.get("irrigation_recommendation"),
fertilization_recommendation=payload.get("fertilization_recommendation"),
)
if progress_callback is not None:
progress_callback(
state="PROGRESS",
meta={"current": 2, "total": 3, "message": "simulation finished"},
)
stage_timeline = summarize_growth_stages(
daily_output=simulation_result.get("daily_output", []),
dynamic_parameters=context.dynamic_parameters,
)
if progress_callback is not None:
progress_callback(
state="PROGRESS",
meta={"current": 3, "total": 3, "message": "growth stages prepared"},
)
paginated = paginate_growth_stages(
stage_timeline,
page=1,
page_size=context.page_size,
)
return {
"plant_name": context.plant_name,
"dynamic_parameters": context.dynamic_parameters,
"engine": _fa_engine_name(simulation_result.get("engine")),
"model_name": _fa_model_name(simulation_result.get("model_name")),
"scenario_id": scenario_id,
"simulation_warning": simulation_error,
"summary_metrics": simulation_result.get("metrics", {}),
"stage_timeline": stage_timeline,
"stages_page": paginated["items"],
"pagination": paginated["pagination"],
"daily_records_count": len(simulation_result.get("daily_output", [])),
"default_page_size": context.page_size,
}
def _estimate_leaf_count(lai: float) -> float:
return max(lai, 0.0) * 12000.0
def _build_current_farm_chart_payload(
context: GrowthSimulationContext,
simulation_result: dict[str, Any],
scenario_id: int | None,
simulation_warning: str | None,
) -> dict[str, Any]:
daily_output = simulation_result.get("daily_output") or []
categories = [str(item.get("DAY")) for item in daily_output]
leaf_count_series = [round(_estimate_leaf_count(_safe_float(item.get("LAI"))), 2) for item in daily_output]
biomass_series = [round(_safe_float(item.get("TAGP")), 2) for item in daily_output]
storage_weight_series = [round(_safe_float(item.get("TWSO")), 2) for item in daily_output]
lai_series = [round(_safe_float(item.get("LAI")), 4) for item in daily_output]
moisture_series = [round(_safe_float(item.get("SM")) * 100.0, 2) for item in daily_output]
latest = daily_output[-1] if daily_output else {}
latest_lai = _safe_float(latest.get("LAI"), 0.0)
latest_biomass = _safe_float(latest.get("TAGP"), 0.0)
latest_storage = _safe_float(latest.get("TWSO"), 0.0)
latest_moisture = _safe_float(latest.get("SM"), 0.0) * 100.0
summary = [
{
"title": "تعداد برگ تخمینی",
"subtitle": "وضعیت فعلی",
"amount": round(_estimate_leaf_count(latest_lai), 2),
"unit": "برگ",
"avatarColor": "success",
"avatarIcon": "tabler-leaf",
},
{
"title": "وزن بیوماس",
"subtitle": "برآورد فعلی",
"amount": round(latest_biomass, 2),
"unit": "کیلوگرم در هکتار",
"avatarColor": "primary",
"avatarIcon": "tabler-chart-bar",
},
{
"title": "وزن محصول",
"subtitle": "برآورد فعلی",
"amount": round(latest_storage, 2),
"unit": "کیلوگرم در هکتار",
"avatarColor": "warning",
"avatarIcon": "tabler-scale",
},
{
"title": "رطوبت خاک",
"subtitle": "آخرین روز",
"amount": round(latest_moisture, 2),
"unit": "%",
"avatarColor": "info",
"avatarIcon": "tabler-droplet",
},
]
return {
"farm_uuid": context.farm_uuid,
"plant_name": context.plant_name,
"engine": _fa_engine_name(simulation_result.get("engine")),
"model_name": _fa_model_name(simulation_result.get("model_name")),
"scenario_id": scenario_id,
"simulation_warning": simulation_warning,
"categories": categories,
"series": [
{"name": "تعداد برگ تخمینی", "key": "leaf_count_estimate", "data": leaf_count_series},
{"name": "وزن بیوماس", "key": "biomass_weight", "data": biomass_series},
{"name": "وزن محصول", "key": "storage_organ_weight", "data": storage_weight_series},
{"name": "شاخص سطح برگ", "key": "lai", "data": lai_series},
{"name": "رطوبت خاک", "key": "soil_moisture_percent", "data": moisture_series},
],
"summary": summary,
"current_state": {
"date": latest.get("DAY"),
"leaf_count_estimate": round(_estimate_leaf_count(latest_lai), 2),
"leaf_area_index": round(latest_lai, 4),
"biomass_weight": round(latest_biomass, 2),
"storage_organ_weight": round(latest_storage, 2),
"soil_moisture_percent": round(latest_moisture, 2),
"development_stage": round(_safe_float(latest.get("DVS"), 0.0), 4),
"gdd": round(_safe_float(latest.get("GDD"), 0.0), 2),
},
"metrics": simulation_result.get("metrics") or {},
"daily_output": daily_output,
}
class CurrentFarmChartSimulator:
"""سازنده chart وضعیت فعلی مزرعه برای خروجی مستقل از dashboard."""
def simulate(
self,
*,
farm_uuid: str,
plant_name: str | None = None,
irrigation_recommendation: dict[str, Any] | None = None,
fertilization_recommendation: dict[str, Any] | None = None,
) -> dict[str, Any]:
if not farm_uuid:
raise GrowthSimulationError("ارسال farm_uuid الزامی است.")
resolved_plant_name = plant_name
if not resolved_plant_name:
sensor = get_canonical_farm_record(farm_uuid)
if sensor is None:
raise GrowthSimulationError("مزرعه پیدا نشد.")
plant = get_runtime_plant_for_farm(sensor)
if plant is None:
raise GrowthSimulationError("گیاهی برای مزرعه انتخاب شده پیدا نشد.")
resolved_plant_name = plant.name
context = build_growth_context(
{
"farm_uuid": farm_uuid,
"plant_name": resolved_plant_name,
"dynamic_parameters": DEFAULT_DYNAMIC_PARAMETERS,
"page_size": DEFAULT_PAGE_SIZE,
}
)
simulation_result, scenario_id, simulation_warning = _run_simulation(
context,
irrigation_recommendation=irrigation_recommendation,
fertilization_recommendation=fertilization_recommendation,
)
return _build_current_farm_chart_payload(
context,
simulation_result,
scenario_id,
simulation_warning,
)
@@ -0,0 +1,172 @@
from __future__ import annotations
from datetime import date, timedelta
from typing import Any
from farm_data.services import get_canonical_farm_record, get_runtime_plant_for_farm
from plant.gdd import resolve_growth_profile
from .growth_simulation import (
DEFAULT_DYNAMIC_PARAMETERS,
DEFAULT_PAGE_SIZE,
GrowthSimulationError,
_run_simulation,
build_growth_context,
)
def _safe_float(value: Any, default: float = 0.0) -> float:
try:
if value in (None, ""):
return default
return float(value)
except (TypeError, ValueError):
return default
def _harvest_description(
*,
plant_name: str,
current_gdd: float,
required_gdd: float,
remaining_gdd: float,
estimated_days: int,
maturity_reached_in_simulation: bool,
) -> str:
if maturity_reached_in_simulation:
return (
f"شبيه ساز رشد نشان مي دهد {plant_name} با روند فعلي در حدود {estimated_days} روز ديگر "
f"به بازه برداشت مي رسد. تا امروز {round(current_gdd, 1)} واحد-روز رشد ثبت شده و "
f"نياز باقيمانده تا بلوغ حدود {round(remaining_gdd, 1)} واحد-روز است."
)
return (
f"شبيه ساز تا انتهاي forecast هنوز به رسيدگي کامل نرسيده، اما با ميانگين رشد فعلي "
f"براورد مي شود {plant_name} حدود {estimated_days} روز ديگر به برداشت برسد. "
f"تا امروز {round(current_gdd, 1)} از {round(required_gdd, 1)} واحد-روز مورد نياز طي شده است."
)
def build_harvest_prediction_payload(
*,
farm_uuid: str,
plant_name: str | None = None,
irrigation_recommendation: dict[str, Any] | None = None,
fertilization_recommendation: dict[str, Any] | None = None,
) -> dict[str, Any]:
resolved_plant_name = plant_name
if not resolved_plant_name:
farm = get_canonical_farm_record(farm_uuid)
if farm is None:
raise GrowthSimulationError("مزرعه پیدا نشد.")
plant = get_runtime_plant_for_farm(farm)
if plant is None:
raise GrowthSimulationError("گیاهی برای مزرعه انتخاب شده پیدا نشد.")
resolved_plant_name = plant.name
context = build_growth_context(
{
"farm_uuid": farm_uuid,
"plant_name": resolved_plant_name,
"dynamic_parameters": DEFAULT_DYNAMIC_PARAMETERS,
"page_size": DEFAULT_PAGE_SIZE,
}
)
simulation_result, scenario_id, simulation_warning = _run_simulation(
context,
irrigation_recommendation=irrigation_recommendation,
fertilization_recommendation=fertilization_recommendation,
)
daily_output = simulation_result.get("daily_output") or []
if not daily_output:
raise GrowthSimulationError("هیچ خروجی شبیه سازی در دسترس نیست.")
profile = resolve_growth_profile(context.plant)
required_gdd = _safe_float(profile.get("required_gdd_for_maturity"), 1200.0)
current_gdd = _safe_float(profile.get("current_cumulative_gdd"), 0.0)
cumulative_gdd = current_gdd
maturity_date = None
daily_gdd_forecast = []
for item in daily_output:
day_gdd = _safe_float(item.get("GDD"), 0.0)
cumulative_gdd += day_gdd
day_value = item.get("DAY")
iso_day = day_value.isoformat() if isinstance(day_value, date) else str(day_value)
daily_gdd_forecast.append(
{
"date": iso_day,
"gdd": round(day_gdd, 3),
"cumulative_gdd": round(cumulative_gdd, 3),
"development_stage": round(_safe_float(item.get("DVS"), 0.0), 4),
}
)
if _safe_float(item.get("DVS"), 0.0) >= 2.0 or cumulative_gdd >= required_gdd:
maturity_date = date.fromisoformat(iso_day)
break
maturity_reached_in_simulation = maturity_date is not None
if maturity_date is None:
last_day = date.fromisoformat(str(daily_output[-1].get("DAY")))
simulated_days = max(len(daily_output), 1)
avg_daily_gdd = max((cumulative_gdd - current_gdd) / simulated_days, 0.0)
remaining_after_simulation = max(required_gdd - cumulative_gdd, 0.0)
extra_days = 0
if avg_daily_gdd > 0 and remaining_after_simulation > 0:
extra_days = int(remaining_after_simulation / avg_daily_gdd)
if remaining_after_simulation % avg_daily_gdd:
extra_days += 1
maturity_date = last_day + timedelta(days=max(extra_days, 0))
remaining_gdd = max(required_gdd - current_gdd, 0.0)
days_until = max((maturity_date - date.today()).days, 0)
window_start = maturity_date - timedelta(days=3)
window_end = maturity_date + timedelta(days=3)
return {
"date": maturity_date.isoformat(),
"dateFormatted": f"{maturity_date.day} {maturity_date.strftime('%B')} {maturity_date.year}",
"daysUntil": days_until,
"description": _harvest_description(
plant_name=context.plant_name,
current_gdd=current_gdd,
required_gdd=required_gdd,
remaining_gdd=remaining_gdd,
estimated_days=days_until,
maturity_reached_in_simulation=maturity_reached_in_simulation,
),
"optimalWindowStart": window_start.isoformat(),
"optimalWindowEnd": window_end.isoformat(),
"gddDetails": {
"current_cumulative_gdd": round(current_gdd, 3),
"required_gdd_for_maturity": round(required_gdd, 3),
"remaining_gdd": round(remaining_gdd, 3),
"estimated_days_to_harvest": days_until,
"predicted_harvest_date": maturity_date.isoformat(),
"predicted_harvest_window": {
"start": window_start.isoformat(),
"end": window_end.isoformat(),
},
"daily_gdd_forecast": daily_gdd_forecast,
"simulation_engine": simulation_result.get("engine"),
"simulation_model_name": simulation_result.get("model_name"),
"simulation_warning": simulation_warning,
"scenario_id": scenario_id,
},
}
class HarvestPredictionService:
def get_harvest_prediction(
self,
*,
farm_uuid: str,
plant_name: str | None = None,
irrigation_recommendation: dict[str, Any] | None = None,
fertilization_recommendation: dict[str, Any] | None = None,
) -> dict[str, Any]:
return build_harvest_prediction_payload(
farm_uuid=farm_uuid,
plant_name=plant_name,
irrigation_recommendation=irrigation_recommendation,
fertilization_recommendation=fertilization_recommendation,
)
@@ -0,0 +1,125 @@
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="SimulationScenario",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(blank=True, default="", max_length=255)),
(
"scenario_type",
models.CharField(
choices=[
("single", "Single Simulation"),
("crop_comparison", "Crop Comparison"),
("fertilization_comparison", "Fertilization Comparison"),
],
db_index=True,
default="single",
max_length=64,
),
),
(
"model_name",
models.CharField(default="Wofost72_WLP_CWB", max_length=128),
),
(
"status",
models.CharField(
choices=[
("pending", "Pending"),
("running", "Running"),
("success", "Success"),
("failure", "Failure"),
],
db_index=True,
default="pending",
max_length=32,
),
),
("input_payload", models.JSONField(blank=True, default=dict)),
("result_payload", models.JSONField(blank=True, default=dict)),
("error_message", models.TextField(blank=True, default="")),
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"ordering": ["-created_at"],
"verbose_name": "Simulation Scenario",
"verbose_name_plural": "Simulation Scenarios",
},
),
migrations.CreateModel(
name="SimulationRun",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("run_key", models.CharField(max_length=64)),
("label", models.CharField(max_length=255)),
(
"status",
models.CharField(
choices=[
("pending", "Pending"),
("running", "Running"),
("success", "Success"),
("failure", "Failure"),
],
db_index=True,
default="pending",
max_length=32,
),
),
("weather_payload", models.JSONField(blank=True, default=list)),
("soil_payload", models.JSONField(blank=True, default=dict)),
("crop_payload", models.JSONField(blank=True, default=dict)),
("site_payload", models.JSONField(blank=True, default=dict)),
("agromanagement_payload", models.JSONField(blank=True, default=list)),
("result_payload", models.JSONField(blank=True, default=dict)),
("error_message", models.TextField(blank=True, default="")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"scenario",
models.ForeignKey(
on_delete=models.deletion.CASCADE,
related_name="runs",
to="crop_simulation.simulationscenario",
),
),
],
options={
"ordering": ["scenario_id", "id"],
"verbose_name": "Simulation Run",
"verbose_name_plural": "Simulation Runs",
},
),
migrations.AddConstraint(
model_name="simulationrun",
constraint=models.UniqueConstraint(
fields=("scenario", "run_key"),
name="crop_simulation_unique_run_key_per_scenario",
),
),
]
@@ -0,0 +1,15 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("crop_simulation", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="simulationscenario",
name="model_name",
field=models.CharField(default="Wofost81_NWLP_CWB_CNB", max_length=128),
),
]
@@ -0,0 +1 @@
+84
View File
@@ -0,0 +1,84 @@
from django.db import models
class SimulationScenario(models.Model):
class ScenarioType(models.TextChoices):
SINGLE = "single", "Single Simulation"
CROP_COMPARISON = "crop_comparison", "Crop Comparison"
FERTILIZATION_COMPARISON = (
"fertilization_comparison",
"Fertilization Comparison",
)
class Status(models.TextChoices):
PENDING = "pending", "Pending"
RUNNING = "running", "Running"
SUCCESS = "success", "Success"
FAILURE = "failure", "Failure"
name = models.CharField(max_length=255, blank=True, default="")
scenario_type = models.CharField(
max_length=64,
choices=ScenarioType.choices,
default=ScenarioType.SINGLE,
db_index=True,
)
model_name = models.CharField(max_length=128, default="Wofost81_NWLP_CWB_CNB")
status = models.CharField(
max_length=32,
choices=Status.choices,
default=Status.PENDING,
db_index=True,
)
input_payload = models.JSONField(default=dict, blank=True)
result_payload = models.JSONField(default=dict, blank=True)
error_message = models.TextField(blank=True, default="")
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-created_at"]
verbose_name = "Simulation Scenario"
verbose_name_plural = "Simulation Scenarios"
def __str__(self):
return self.name or f"{self.scenario_type}:{self.pk}"
class SimulationRun(models.Model):
scenario = models.ForeignKey(
SimulationScenario,
on_delete=models.CASCADE,
related_name="runs",
)
run_key = models.CharField(max_length=64)
label = models.CharField(max_length=255)
status = models.CharField(
max_length=32,
choices=SimulationScenario.Status.choices,
default=SimulationScenario.Status.PENDING,
db_index=True,
)
weather_payload = models.JSONField(default=list, blank=True)
soil_payload = models.JSONField(default=dict, blank=True)
crop_payload = models.JSONField(default=dict, blank=True)
site_payload = models.JSONField(default=dict, blank=True)
agromanagement_payload = models.JSONField(default=list, blank=True)
result_payload = models.JSONField(default=dict, blank=True)
error_message = models.TextField(blank=True, default="")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["scenario_id", "id"]
constraints = [
models.UniqueConstraint(
fields=["scenario", "run_key"],
name="crop_simulation_unique_run_key_per_scenario",
)
]
verbose_name = "Simulation Run"
verbose_name_plural = "Simulation Runs"
def __str__(self):
return f"{self.scenario_id}:{self.run_key}"
@@ -0,0 +1,801 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import date
from statistics import mean
from typing import Any
from django.apps import apps
from location_data.satellite_snapshot import build_location_satellite_snapshot
from crop_simulation.services import CropSimulationService
def _safe_float(value: Any, default: float = 0.0) -> float:
try:
if value is None or value == "":
return default
return float(value)
except (TypeError, ValueError):
return default
def _mm_to_cm_day(value: Any, default: float) -> float:
scaled = _safe_float(value, default * 10.0) / 10.0
return round(max(scaled, 0.0), 4)
def _clamp(value: float, lower: float, upper: float) -> float:
return max(lower, min(value, upper))
def _stage_key(growth_stage: str | None) -> str:
text = (growth_stage or "").strip().lower()
if any(token in text for token in ("flower", "گل", "anthesis")):
return "flowering"
if any(token in text for token in ("fruit", "میوه", "برداشت", "ripen", "harvest")):
return "fruiting"
if any(token in text for token in ("initial", "seed", "جوانه", "نشا", "استقرار")):
return "initial"
return "vegetative"
def _first_not_none(*values: Any) -> Any:
for value in values:
if value is not None:
return value
return None
def _sensor_metric(sensor: Any, metric: str) -> float | None:
if sensor is None:
return None
if hasattr(sensor, metric):
value = getattr(sensor, metric)
return _safe_float(value, default=0.0) if value is not None else None
payload = getattr(sensor, "sensor_payload", None) or {}
if not isinstance(payload, dict):
return None
for block in payload.values():
if isinstance(block, dict) and block.get(metric) is not None:
return _safe_float(block.get(metric), default=0.0)
return None
def _parse_temperature_range(plant: Any) -> tuple[float, float]:
raw = (getattr(plant, "temperature", "") or "").replace("تا", "-")
digits = []
current = ""
for char in raw:
if char.isdigit() or char in ".-":
current += char
continue
if current:
digits.append(current)
current = ""
if current:
digits.append(current)
if len(digits) >= 2:
low = _safe_float(digits[0], 12.0)
high = _safe_float(digits[1], 28.0)
if low < high:
return low, high
return 14.0, 30.0
def _mean_forecast_value(forecasts: list[Any], attr: str, fallback: float = 0.0) -> float:
values = [_safe_float(getattr(item, attr, None), default=fallback) for item in forecasts]
return round(mean(values), 3) if values else fallback
def _next_rain_date(forecasts: list[Any], threshold_mm: float) -> str | None:
for forecast in forecasts:
if _safe_float(getattr(forecast, "precipitation", None), 0.0) >= threshold_mm:
return forecast.forecast_date.isoformat()
return None
def _best_timing(avg_temp: float, avg_wind: float) -> str:
if avg_temp >= 30 or avg_wind >= 18:
return "اوایل صبح"
if avg_temp <= 18:
return "اواخر صبح"
return "اوایل صبح یا نزدیک غروب"
def _build_weather_records(forecasts: list[Any], *, latitude: float, longitude: float) -> list[dict[str, Any]]:
records = []
for forecast in forecasts:
tmin = _safe_float(
_first_not_none(getattr(forecast, "temperature_min", None), getattr(forecast, "temperature_mean", None)),
12.0,
)
tmax = _safe_float(
_first_not_none(getattr(forecast, "temperature_max", None), getattr(forecast, "temperature_mean", None)),
24.0,
)
humidity = _safe_float(getattr(forecast, "humidity_mean", None), 55.0)
vap = max(6.0, round((humidity / 100.0) * 20.0, 3))
wind_kmh = _safe_float(getattr(forecast, "wind_speed_max", None), 7.2)
wind_ms = round(wind_kmh / 3.6, 3)
et0 = _mm_to_cm_day(getattr(forecast, "et0", None), 0.35)
records.append(
{
"DAY": forecast.forecast_date,
"LAT": latitude,
"LON": longitude,
"ELEV": 1200.0,
"IRRAD": 16_000_000.0,
"TMIN": tmin,
"TMAX": tmax,
"VAP": vap,
"WIND": wind_ms,
"RAIN": _mm_to_cm_day(getattr(forecast, "precipitation", None), 0.0),
"E0": et0,
"ES0": max(et0 * 0.9, 0.1),
"ET0": et0,
}
)
return records
def _build_soil_parameters(sensor: Any) -> tuple[dict[str, Any], dict[str, Any]]:
moisture_pct = _sensor_metric(sensor, "soil_moisture")
center_location = getattr(sensor, "center_location", None)
satellite_metrics = (
build_location_satellite_snapshot(center_location).get("resolved_metrics") or {}
if center_location is not None
else {}
)
ndwi = _safe_float(satellite_metrics.get("ndwi"), 0.34)
wv0033 = ndwi if ndwi > 0 else 0.34
wv1500 = max(round(wv0033 * 0.45, 3), 0.14)
smfcf = _clamp(wv0033 if wv0033 > 0 else 0.34, 0.2, 0.55)
smw = _clamp(wv1500 if wv1500 > 0 else 0.12, 0.05, smfcf - 0.02)
if moisture_pct is not None:
wav = round(_clamp(moisture_pct / 100.0, smw, smfcf) * 100.0, 3)
else:
wav = round(((smfcf + smw) / 2.0) * 100.0, 3)
soil = {
"SMFCF": round(smfcf, 3),
"SMW": round(smw, 3),
"RDMSOL": 120.0,
}
site = {"WAV": wav}
return soil, site
def _build_crop_parameters(plant: Any, growth_stage: str | None) -> tuple[dict[str, Any], list[dict[str, Any]]] | None:
profiles = []
for attr in ("growth_profile", "irrigation_profile", "health_profile"):
profile = getattr(plant, attr, None) or {}
if isinstance(profile, dict):
profiles.append(profile)
simulation_block = None
for profile in profiles:
candidate = profile.get("simulation")
if isinstance(candidate, dict):
simulation_block = candidate
break
if not simulation_block:
return None
crop_parameters = simulation_block.get("crop_parameters")
agromanagement = simulation_block.get("agromanagement")
if not isinstance(crop_parameters, dict) or not agromanagement:
return None
enriched_crop = dict(crop_parameters)
enriched_crop.setdefault("crop_name", getattr(plant, "name", "crop"))
if growth_stage:
enriched_crop.setdefault("growth_stage", growth_stage)
return enriched_crop, agromanagement
def _event_dates_for_frequency(forecasts: list[Any], count: int) -> list[str]:
if not forecasts:
return []
ranked = sorted(
forecasts,
key=lambda item: (
_safe_float(getattr(item, "et0", None), 0.0)
+ _safe_float(getattr(item, "temperature_max", None), 0.0) / 10.0
- _safe_float(getattr(item, "precipitation", None), 0.0)
),
reverse=True,
)
selected = sorted(ranked[:count], key=lambda item: item.forecast_date)
return [item.forecast_date.isoformat() for item in selected]
def _irrigation_context_text(result: dict[str, Any]) -> str:
recommended = result["recommended_strategy"]
alternative_lines = [
f"- {item['label']}: امتیاز {item['score']}, آب کل {item['total_irrigation_mm']} mm"
for item in result.get("alternatives", [])
]
lines = [
f"engine: {result['engine']}",
f"استراتژی منتخب: {recommended['label']}",
f"امتیاز شبیه سازی: {recommended['score']}",
f"شاخص عملکرد مورد انتظار: {recommended['expected_yield_index']}",
f"آب کل پیشنهادی: {recommended['total_irrigation_mm']} mm",
f"مقدار هر نوبت: {recommended['amount_per_event_mm']} mm",
f"تعداد نوبت: {recommended['events']}",
f"تقویم اجرای پیشنهادی: {', '.join(recommended['event_dates']) or 'نامشخص'}",
f"زمان انجام: {recommended['timing']}",
f"رطوبت هدف خاک: {recommended['moisture_target_percent']}%",
f"اعتبار: {recommended['validity_period']}",
"دلایل اصلی:",
*[f"- {item}" for item in recommended["reasoning"]],
]
if alternative_lines:
lines.extend(["گزینه های جایگزین:", *alternative_lines])
return "\n".join(lines)
def _fertilization_context_text(result: dict[str, Any]) -> str:
recommended = result["recommended_strategy"]
alternative_lines = [
f"- {item['label']}: امتیاز {item['score']}, دوز {item['amount_kg_per_ha']} kg/ha"
for item in result.get("alternatives", [])
]
lines = [
f"engine: {result['engine']}",
f"استراتژی منتخب: {recommended['label']}",
f"امتیاز شبیه سازی: {recommended['score']}",
f"شاخص عملکرد مورد انتظار: {recommended['expected_yield_index']}",
f"نوع کود: {recommended['fertilizer_type']}",
f"مقدار مصرف: {recommended['amount_kg_per_ha']} kg/ha",
f"روش مصرف: {recommended['application_method']}",
f"زمان مصرف: {recommended['timing']}",
f"اعتبار: {recommended['validity_period']}",
"دلایل اصلی:",
*[f"- {item}" for item in recommended["reasoning"]],
]
if alternative_lines:
lines.extend(["گزینه های جایگزین:", *alternative_lines])
return "\n".join(lines)
@dataclass
class StrategyResult:
code: str
label: str
score: float
expected_yield_index: float
payload: dict[str, Any]
reasoning: list[str]
class SimulationRecommendationOptimizer:
"""بهینه ساز توصیه های آبیاری و کودهی داخل اپ crop_simulation."""
def __init__(self):
self.simulation_service = CropSimulationService()
def optimize_irrigation(
self,
*,
sensor: Any,
plant: Any,
forecasts: list[Any],
daily_water_needs: list[dict[str, Any]],
growth_stage: str | None,
irrigation_method: Any | None,
) -> dict[str, Any] | None:
if sensor is None or plant is None or not forecasts:
return None
crop_blueprint = _build_crop_parameters(plant, growth_stage)
if crop_blueprint:
pcse_result = self._optimize_irrigation_with_pcse(
sensor=sensor,
plant=plant,
forecasts=forecasts,
daily_water_needs=daily_water_needs,
growth_stage=growth_stage,
crop_blueprint=crop_blueprint,
)
if pcse_result is not None:
return pcse_result
return self._optimize_irrigation_with_heuristic(
sensor=sensor,
plant=plant,
forecasts=forecasts,
daily_water_needs=daily_water_needs,
growth_stage=growth_stage,
irrigation_method=irrigation_method,
)
def optimize_fertilization(
self,
*,
sensor: Any,
plant: Any,
forecasts: list[Any],
growth_stage: str | None,
) -> dict[str, Any] | None:
if sensor is None or plant is None:
return None
crop_blueprint = _build_crop_parameters(plant, growth_stage)
if crop_blueprint and forecasts:
pcse_result = self._optimize_fertilization_with_pcse(
sensor=sensor,
plant=plant,
forecasts=forecasts,
growth_stage=growth_stage,
crop_blueprint=crop_blueprint,
)
if pcse_result is not None:
return pcse_result
return self._optimize_fertilization_with_heuristic(
sensor=sensor,
plant=plant,
forecasts=forecasts,
growth_stage=growth_stage,
)
def _optimize_irrigation_with_pcse(
self,
*,
sensor: Any,
plant: Any,
forecasts: list[Any],
daily_water_needs: list[dict[str, Any]],
growth_stage: str | None,
crop_blueprint: tuple[dict[str, Any], list[dict[str, Any]]],
) -> dict[str, Any] | None:
defaults = apps.get_app_config("irrigation").get_optimizer_defaults()
crop_parameters, agromanagement = crop_blueprint
soil, site = _build_soil_parameters(sensor)
weather = _build_weather_records(
forecasts,
latitude=_safe_float(sensor.center_location.latitude),
longitude=_safe_float(sensor.center_location.longitude),
)
total_mm = round(sum(_safe_float(item.get("gross_irrigation_mm"), 0.0) for item in daily_water_needs), 3)
if total_mm <= 0:
return None
strategies = []
for spec in defaults["strategy_profiles"]:
irrigation_events = []
event_dates = _event_dates_for_frequency(forecasts, max(1, spec["event_count"]))
amount_per_event = round((total_mm * spec["multiplier"]) / max(len(event_dates), 1), 3)
for day in event_dates:
irrigation_events.append({"date": day, "amount": amount_per_event})
try:
result = self.simulation_service.run_single_simulation(
farm_uuid=str(sensor.farm_uuid),
plant_name=getattr(plant, "name", None),
weather=weather,
crop_parameters=crop_parameters,
agromanagement=agromanagement,
soil=soil,
site_parameters=site,
irrigation_recommendation={"events": irrigation_events},
name=f"irrigation-{spec['code']}",
)
except Exception:
return None
yield_estimate = _safe_float(
result.get("result", {}).get("metrics", {}).get("yield_estimate"),
0.0,
)
score = round(_clamp((yield_estimate / 100.0), 0.0, 100.0), 2)
strategies.append(
StrategyResult(
code=spec["code"],
label=spec["label"],
score=score,
expected_yield_index=round(score, 2),
payload={
"events": len(event_dates),
"event_dates": event_dates,
"amount_per_event_mm": amount_per_event,
"total_irrigation_mm": round(amount_per_event * len(event_dates), 3),
"timing": _best_timing(
_mean_forecast_value(forecasts, "temperature_mean", 22.0),
_mean_forecast_value(forecasts, "wind_speed_max", 8.0),
),
},
reasoning=[
"امتیاز بر اساس بیشترین عملکرد شبیه سازی شده انتخاب شد.",
f"عملکرد نسبی این سناریو {round(score, 2)} ارزیابی شد.",
],
)
)
best = max(strategies, key=lambda item: item.score)
moisture_target = defaults["stage_targets"].get(_stage_key(growth_stage), defaults["stage_targets"]["vegetative"])
result = {
"engine": "pcse",
"recommended_strategy": {
"code": best.code,
"label": best.label,
"score": best.score,
"expected_yield_index": best.expected_yield_index,
"total_irrigation_mm": best.payload["total_irrigation_mm"],
"amount_per_event_mm": best.payload["amount_per_event_mm"],
"events": best.payload["events"],
"frequency_per_week": best.payload["events"],
"event_dates": best.payload["event_dates"],
"timing": best.payload["timing"],
"moisture_target_percent": moisture_target,
"validity_period": f"معتبر برای {defaults['validity_days']} روز آینده",
"reasoning": best.reasoning,
},
"alternatives": [
{
"code": item.code,
"label": item.label,
"score": item.score,
"expected_yield_index": item.expected_yield_index,
"total_irrigation_mm": item.payload["total_irrigation_mm"],
}
for item in sorted(strategies, key=lambda value: value.score, reverse=True)
if item.code != best.code
],
}
result["context_text"] = _irrigation_context_text(result)
return result
def _optimize_irrigation_with_heuristic(
self,
*,
sensor: Any,
plant: Any,
forecasts: list[Any],
daily_water_needs: list[dict[str, Any]],
growth_stage: str | None,
irrigation_method: Any | None,
) -> dict[str, Any]:
defaults = apps.get_app_config("irrigation").get_optimizer_defaults()
stage_key = _stage_key(growth_stage)
moisture_target = defaults["stage_targets"].get(stage_key, defaults["stage_targets"]["vegetative"])
total_mm = round(sum(_safe_float(item.get("gross_irrigation_mm"), 0.0) for item in daily_water_needs), 3)
non_zero_days = [item for item in daily_water_needs if _safe_float(item.get("gross_irrigation_mm"), 0.0) > 0]
average_temp = _mean_forecast_value(forecasts, "temperature_mean", 22.0)
average_wind = _mean_forecast_value(forecasts, "wind_speed_max", 8.0)
heat_risk = _mean_forecast_value(forecasts, "temperature_max", 28.0) >= 32.0
rain_date = _next_rain_date(forecasts, defaults["significant_rain_threshold_mm"])
efficiency = _safe_float(getattr(irrigation_method, "water_efficiency_percent", None), 75.0)
soil_moisture = _sensor_metric(sensor, "soil_moisture")
strategies: list[StrategyResult] = []
for spec in defaults["strategy_profiles"]:
event_count = max(1, min(7, round(max(len(non_zero_days), 1) * spec["frequency_factor"])))
applied_total = round(max(total_mm * spec["multiplier"], 0.0), 3)
amount_per_event = round(max(applied_total / event_count, defaults["minimum_event_mm"]), 3)
water_penalty = abs(applied_total - total_mm) * 2.4
if total_mm <= 0:
water_penalty = 0.0 if spec["code"] == "conservative" else 12.0
soil_penalty = 0.0
if soil_moisture is not None:
if soil_moisture < 25 and spec["code"] == "conservative":
soil_penalty += 8.0
if soil_moisture > 55 and spec["code"] == "protective":
soil_penalty += 7.0
climate_bonus = 0.0
if heat_risk and spec["code"] == "protective":
climate_bonus += 6.0
if rain_date and spec["code"] == "protective":
climate_bonus -= 8.0
if efficiency >= 85 and spec["code"] == "balanced":
climate_bonus += 4.0
score = round(_clamp(100.0 - water_penalty - soil_penalty + climate_bonus, 35.0, 96.0), 2)
event_dates = _event_dates_for_frequency(forecasts, event_count)
reasoning = [
f"نیاز آبی محاسبه شده برای بازه پیش رو حدود {total_mm} میلی متر است.",
f"این سناریو {applied_total} میلی متر آب را در {event_count} نوبت پخش می کند.",
]
if heat_risk:
reasoning.append("به خاطر دمای بالاتر از حد مطلوب، تنش گرمایی در امتیازدهی لحاظ شده است.")
if rain_date:
reasoning.append(f"بارش معنی دار از تاریخ {rain_date} احتمال کاهش نیاز آبی را بالا می برد.")
if soil_moisture is not None:
reasoning.append(f"رطوبت فعلی خاک حدود {round(soil_moisture, 1)} درصد در نظر گرفته شده است.")
strategies.append(
StrategyResult(
code=spec["code"],
label=spec["label"],
score=score,
expected_yield_index=round(52.0 + (score * 0.48), 2),
payload={
"events": event_count,
"amount_per_event_mm": amount_per_event,
"total_irrigation_mm": applied_total,
"event_dates": event_dates,
"timing": _best_timing(average_temp, average_wind),
},
reasoning=reasoning,
)
)
best = max(strategies, key=lambda item: item.score)
validity_period = f"معتبر برای {defaults['validity_days']} روز آینده"
if rain_date:
validity_period = f"معتبر تا قبل از بارش موثر پیش بینی شده در {rain_date}"
result = {
"engine": "crop_simulation_heuristic",
"recommended_strategy": {
"code": best.code,
"label": best.label,
"score": best.score,
"expected_yield_index": best.expected_yield_index,
"total_irrigation_mm": best.payload["total_irrigation_mm"],
"amount_per_event_mm": best.payload["amount_per_event_mm"],
"events": best.payload["events"],
"frequency_per_week": min(best.payload["events"] + 1, 7),
"event_dates": best.payload["event_dates"],
"timing": best.payload["timing"],
"moisture_target_percent": moisture_target,
"validity_period": validity_period,
"reasoning": best.reasoning,
},
"alternatives": [
{
"code": item.code,
"label": item.label,
"score": item.score,
"expected_yield_index": item.expected_yield_index,
"total_irrigation_mm": item.payload["total_irrigation_mm"],
}
for item in sorted(strategies, key=lambda value: value.score, reverse=True)
if item.code != best.code
],
}
result["context_text"] = _irrigation_context_text(result)
return result
def _optimize_fertilization_with_pcse(
self,
*,
sensor: Any,
plant: Any,
forecasts: list[Any],
growth_stage: str | None,
crop_blueprint: tuple[dict[str, Any], list[dict[str, Any]]],
) -> dict[str, Any] | None:
defaults = apps.get_app_config("fertilization").get_optimizer_defaults()
crop_parameters, agromanagement = crop_blueprint
soil, site = _build_soil_parameters(sensor)
weather = _build_weather_records(
forecasts,
latitude=_safe_float(sensor.center_location.latitude),
longitude=_safe_float(sensor.center_location.longitude),
)
stage_key = _stage_key(growth_stage)
target = defaults["stage_targets"].get(stage_key, defaults["stage_targets"]["vegetative"])
base_n = max(target["n"], 20)
strategies = []
for spec in defaults["strategy_profiles"]:
n_amount = round(base_n * spec["multiplier"], 3)
fertilizer_formula = spec["formula_override"] or target["formula"]
strategy_agromanagement = [
{
key: {
**value,
"TimedEvents": [
{
"event_signal": "apply_n",
"name": spec["label"],
"events_table": [
{
forecasts[0].forecast_date: {
"N_amount": n_amount,
"N_recovery": 0.7,
}
}
],
}
],
}
}
for entry in agromanagement
for key, value in entry.items()
] or agromanagement
try:
result = self.simulation_service.run_single_simulation(
farm_uuid=str(sensor.farm_uuid),
plant_name=getattr(plant, "name", None),
weather=weather,
crop_parameters=crop_parameters,
agromanagement=strategy_agromanagement,
soil=soil,
site_parameters=site,
name=f"fertilization-{spec['code']}",
)
except Exception:
return None
yield_estimate = _safe_float(
result.get("result", {}).get("metrics", {}).get("yield_estimate"),
0.0,
)
score = round(_clamp((yield_estimate / 100.0), 0.0, 100.0), 2)
strategies.append(
StrategyResult(
code=spec["code"],
label=spec["label"],
score=score,
expected_yield_index=score,
payload={
"amount_kg_per_ha": round(n_amount * 1.6, 3),
"fertilizer_type": fertilizer_formula,
"application_method": target["application_method"],
"timing": target["timing"],
},
reasoning=[
"سناریو برتر با بیشترین عملکرد شبیه سازی شده انتخاب شد.",
f"فرمول هدف برای این مرحله {target['formula']} در نظر گرفته شد.",
],
)
)
best = max(strategies, key=lambda item: item.score)
result = {
"engine": "pcse",
"recommended_strategy": {
"code": best.code,
"label": best.label,
"score": best.score,
"expected_yield_index": best.expected_yield_index,
"fertilizer_type": best.payload["fertilizer_type"],
"amount_kg_per_ha": best.payload["amount_kg_per_ha"],
"application_method": best.payload["application_method"],
"timing": best.payload["timing"],
"validity_period": f"معتبر برای {defaults['validity_days']} روز آینده",
"reasoning": best.reasoning,
},
"alternatives": [
{
"code": item.code,
"label": item.label,
"score": item.score,
"expected_yield_index": item.expected_yield_index,
"fertilizer_type": item.payload["fertilizer_type"],
"amount_kg_per_ha": item.payload["amount_kg_per_ha"],
"application_method": item.payload["application_method"],
"timing": item.payload["timing"],
"reasoning": item.reasoning,
}
for item in sorted(strategies, key=lambda value: value.score, reverse=True)
if item.code != best.code
],
}
result["context_text"] = _fertilization_context_text(result)
return result
def _optimize_fertilization_with_heuristic(
self,
*,
sensor: Any,
plant: Any,
forecasts: list[Any],
growth_stage: str | None,
) -> dict[str, Any]:
defaults = apps.get_app_config("fertilization").get_optimizer_defaults()
stage_key = _stage_key(growth_stage)
target = defaults["stage_targets"].get(stage_key, defaults["stage_targets"]["vegetative"])
current_n = _sensor_metric(sensor, "nitrogen")
current_p = _sensor_metric(sensor, "phosphorus")
current_k = _sensor_metric(sensor, "potassium")
current_ph = _sensor_metric(sensor, "soil_ph")
deficits = {
"n": max(target["n"] - _safe_float(current_n, target["n"] * 0.6), 0.0),
"p": max(target["p"] - _safe_float(current_p, target["p"] * 0.6), 0.0),
"k": max(target["k"] - _safe_float(current_k, target["k"] * 0.6), 0.0),
}
dominant = max(deficits, key=deficits.get)
severity = sum(deficits.values())
next_rain = _next_rain_date(forecasts, defaults["rain_delay_threshold_mm"]) if forecasts else None
avg_temp = _mean_forecast_value(forecasts, "temperature_mean", 22.0)
strategies: list[StrategyResult] = []
for spec in defaults["strategy_profiles"]:
base_amount = max(30.0, min(120.0, 35.0 + (severity * 1.4)))
amount = round(base_amount * spec["multiplier"], 2)
mismatch_penalty = 0.0
if dominant == "n" and "ازت" not in spec["focus"]:
mismatch_penalty += 12.0
if dominant == "k" and "پتاس" not in spec["focus"]:
mismatch_penalty += 12.0
if dominant == "p" and "فسفر" not in spec["focus"]:
mismatch_penalty += 12.0
if current_ph is not None and current_ph > 7.8 and "فسفر" in spec["focus"]:
mismatch_penalty += 8.0
if next_rain and spec["application_method"] == "محلول پاشی":
mismatch_penalty += 10.0
score = round(_clamp(96.0 - mismatch_penalty - abs(spec["multiplier"] - 1.0) * 18.0, 42.0, 95.0), 2)
reasoning = [
f"کسری عناصر برای این مرحله با فرمول هدف {target['formula']} سنجیده شد.",
f"بیشترین کمبود نسبی مربوط به عنصر {dominant.upper()} است.",
f"دوز پیشنهادی این سناریو {amount} کیلوگرم در هکتار برآورد شد.",
]
if current_ph is not None:
reasoning.append(f"pH فعلی خاک حدود {round(current_ph, 2)} در تصمیم گیری لحاظ شد.")
if next_rain:
reasoning.append(f"به دلیل بارش موثر نزدیک در {next_rain} از مصرف سطحی پرریسک اجتناب شده است.")
strategies.append(
StrategyResult(
code=spec["code"],
label=spec["label"],
score=score,
expected_yield_index=round(50.0 + (score * 0.5), 2),
payload={
"fertilizer_type": spec["formula_override"] or target["formula"],
"amount_kg_per_ha": amount,
"application_method": spec["application_method"],
"timing": target["timing"] if avg_temp < 30 else "صبح زود یا نزدیک غروب",
},
reasoning=reasoning,
)
)
best = max(strategies, key=lambda item: item.score)
validity_period = f"معتبر برای {defaults['validity_days']} روز آینده"
if stage_key == "flowering":
validity_period = "معتبر تا پایان پنجره گلدهی فعلی و حداکثر 5 روز آینده"
result = {
"engine": "crop_simulation_heuristic",
"recommended_strategy": {
"code": best.code,
"label": best.label,
"score": best.score,
"expected_yield_index": best.expected_yield_index,
"fertilizer_type": best.payload["fertilizer_type"],
"amount_kg_per_ha": best.payload["amount_kg_per_ha"],
"application_method": best.payload["application_method"],
"timing": best.payload["timing"],
"validity_period": validity_period,
"reasoning": best.reasoning,
},
"alternatives": [
{
"code": item.code,
"label": item.label,
"score": item.score,
"expected_yield_index": item.expected_yield_index,
"fertilizer_type": item.payload["fertilizer_type"],
"amount_kg_per_ha": item.payload["amount_kg_per_ha"],
"application_method": item.payload["application_method"],
"timing": item.payload["timing"],
"reasoning": item.reasoning,
}
for item in sorted(strategies, key=lambda value: value.score, reverse=True)
if item.code != best.code
],
"nutrient_status": {
"nitrogen": current_n,
"phosphorus": current_p,
"potassium": current_k,
"soil_ph": current_ph,
"dominant_gap": dominant,
},
}
result["context_text"] = _fertilization_context_text(result)
return result
+184
View File
@@ -0,0 +1,184 @@
from __future__ import annotations
import json
from rest_framework import serializers
class QueryJSONField(serializers.JSONField):
def to_internal_value(self, data):
if isinstance(data, str):
data = data.strip()
if not data:
return None
try:
data = json.loads(data)
except json.JSONDecodeError as exc:
raise serializers.ValidationError("فرمت JSON نامعتبر است.") from exc
return super().to_internal_value(data)
class GrowthSimulationRequestSerializer(serializers.Serializer):
plant_name = serializers.CharField(help_text="نام گیاه")
dynamic_parameters = serializers.ListField(
child=serializers.CharField(),
allow_empty=False,
help_text="پارامترهای متغیر رشد که باید در خروجی گزارش شوند.",
)
farm_uuid = serializers.UUIDField(required=False, allow_null=True)
weather = serializers.JSONField(required=False)
soil_parameters = serializers.JSONField(required=False)
site_parameters = serializers.JSONField(required=False)
crop_parameters = serializers.JSONField(required=False)
agromanagement = serializers.JSONField(required=False)
irrigation_recommendation = serializers.JSONField(required=False)
fertilization_recommendation = serializers.JSONField(required=False)
page_size = serializers.IntegerField(required=False, min_value=1, max_value=50)
def validate(self, attrs):
if not attrs.get("farm_uuid") and not attrs.get("weather"):
raise serializers.ValidationError(
"یکی از farm_uuid یا weather باید ارسال شود."
)
return attrs
class GrowthSimulationQueuedSerializer(serializers.Serializer):
task_id = serializers.CharField()
status_url = serializers.CharField()
plant_name = serializers.CharField()
class GrowthStageMetricSerializer(serializers.Serializer):
start = serializers.FloatField()
end = serializers.FloatField()
min = serializers.FloatField()
max = serializers.FloatField()
avg = serializers.FloatField()
class GrowthStageSerializer(serializers.Serializer):
order = serializers.IntegerField()
stage_code = serializers.CharField()
stage_name = serializers.CharField()
start_date = serializers.DateField()
end_date = serializers.DateField()
days_count = serializers.IntegerField()
metrics = serializers.JSONField()
class GrowthPaginationSerializer(serializers.Serializer):
page = serializers.IntegerField()
page_size = serializers.IntegerField()
total_items = serializers.IntegerField()
total_pages = serializers.IntegerField()
has_next = serializers.BooleanField()
has_previous = serializers.BooleanField()
class GrowthSimulationResultSerializer(serializers.Serializer):
plant_name = serializers.CharField()
dynamic_parameters = serializers.ListField(child=serializers.CharField())
engine = serializers.CharField(allow_null=True)
model_name = serializers.CharField(allow_null=True)
scenario_id = serializers.IntegerField(allow_null=True)
simulation_warning = serializers.CharField(allow_null=True, allow_blank=True)
summary_metrics = serializers.JSONField()
stage_timeline = GrowthStageSerializer(many=True)
stages_page = GrowthStageSerializer(many=True)
pagination = GrowthPaginationSerializer()
daily_records_count = serializers.IntegerField()
default_page_size = serializers.IntegerField()
class CurrentFarmChartRequestSerializer(serializers.Serializer):
farm_uuid = serializers.UUIDField(help_text="شناسه یکتای مزرعه")
plant_name = serializers.CharField(required=False, allow_blank=True, help_text="نام گیاه")
irrigation_recommendation = serializers.JSONField(required=False)
fertilization_recommendation = serializers.JSONField(required=False)
class CurrentFarmChartResponseSerializer(serializers.Serializer):
farm_uuid = serializers.CharField(allow_null=True)
plant_name = serializers.CharField()
engine = serializers.CharField(allow_null=True)
model_name = serializers.CharField(allow_null=True)
scenario_id = serializers.IntegerField(allow_null=True)
simulation_warning = serializers.CharField(allow_null=True, allow_blank=True)
categories = serializers.ListField(child=serializers.CharField())
series = serializers.JSONField()
summary = serializers.JSONField()
current_state = serializers.JSONField()
metrics = serializers.JSONField()
daily_output = serializers.JSONField()
class HarvestPredictionRequestSerializer(serializers.Serializer):
farm_uuid = serializers.UUIDField(help_text="شناسه یکتای مزرعه")
plant_name = serializers.CharField(required=False, allow_blank=True, help_text="نام گیاه")
irrigation_recommendation = serializers.JSONField(required=False)
fertilization_recommendation = serializers.JSONField(required=False)
class HarvestPredictionResponseSerializer(serializers.Serializer):
date = serializers.CharField()
dateFormatted = serializers.CharField()
daysUntil = serializers.IntegerField()
description = serializers.CharField()
optimalWindowStart = serializers.CharField()
optimalWindowEnd = serializers.CharField()
gddDetails = serializers.JSONField()
class YieldPredictionRequestSerializer(serializers.Serializer):
farm_uuid = serializers.UUIDField(help_text="شناسه یکتای مزرعه")
plant_name = serializers.CharField(required=False, allow_blank=True, help_text="نام گیاه")
irrigation_recommendation = serializers.JSONField(required=False)
fertilization_recommendation = serializers.JSONField(required=False)
class YieldPredictionResponseSerializer(serializers.Serializer):
farm_uuid = serializers.CharField()
plant_name = serializers.CharField(allow_null=True)
predictedYieldTons = serializers.FloatField()
predictedYieldRaw = serializers.FloatField()
unit = serializers.CharField()
sourceUnit = serializers.CharField()
simulationEngine = serializers.CharField(allow_null=True)
simulationModel = serializers.CharField(allow_null=True)
scenarioId = serializers.IntegerField(allow_null=True)
simulationWarning = serializers.CharField(allow_null=True, allow_blank=True)
supportingMetrics = serializers.JSONField()
class YieldHarvestSummaryQuerySerializer(serializers.Serializer):
farm_uuid = serializers.UUIDField(required=True, help_text="شناسه یکتای مزرعه")
season_year = serializers.IntegerField(required=False, help_text="سال زراعی")
crop_name = serializers.CharField(required=False, allow_blank=True, help_text="نام محصول")
include_narrative = serializers.BooleanField(
required=False,
default=False,
help_text="در صورت true بودن، بخش روایت نیز در آینده اضافه می شود.",
)
irrigation_recommendation = QueryJSONField(
required=False,
allow_null=True,
help_text="برنامه آبیاری به صورت JSON برای تزریق به PCSE.",
)
fertilization_recommendation = QueryJSONField(
required=False,
allow_null=True,
help_text="برنامه کودهی به صورت JSON برای تزریق به PCSE.",
)
class YieldHarvestSummaryResponseSerializer(serializers.Serializer):
farm_uuid = serializers.CharField()
season_highlights_card = serializers.JSONField()
yield_prediction = serializers.JSONField()
harvest_prediction_card = serializers.JSONField()
harvest_readiness_zones = serializers.JSONField()
yield_quality_bands = serializers.JSONField()
harvest_operations_card = serializers.JSONField()
yield_prediction_chart = serializers.JSONField()
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
from __future__ import annotations
from config.celery import app
from .growth_simulation import run_growth_simulation
@app.task(bind=True)
def run_growth_simulation_task(self, payload: dict) -> dict:
return run_growth_simulation(
payload,
progress_callback=self.update_state,
)

Some files were not shown because too many files have changed in this diff Show More