This commit is contained in:
2026-05-13 16:45:54 +03:30
parent 948c062b93
commit 46fe62fa04
96 changed files with 3834 additions and 155 deletions
+11 -1
View File
@@ -13,7 +13,12 @@ from typing import Any
from django.apps import apps
from farm_data.models import SensorData
from farm_data.services import clone_snapshot_as_runtime_plant, get_farm_plant_snapshot_by_name
from farm_data.services import (
build_ai_farm_snapshot,
clone_snapshot_as_runtime_plant,
get_ai_snapshot_weather,
get_farm_plant_snapshot_by_name,
)
from rag.api_provider import get_chat_client
from rag.chat import (
_complete_audit_log,
@@ -628,6 +633,7 @@ def get_fertilization_recommendation(
.filter(farm_uuid=resolved_farm_uuid)
.first()
)
ai_snapshot = build_ai_farm_snapshot(resolved_farm_uuid)
plant_config = apps.get_app_config("plant")
resolved_plant_name = plant_config.resolve_plant_name(plant_name)
@@ -662,6 +668,7 @@ def get_fertilization_recommendation(
plant=plant,
forecasts=forecasts,
growth_stage=resolved_growth_stage,
ai_snapshot=ai_snapshot,
)
context = build_rag_context(
@@ -727,6 +734,9 @@ def get_fertilization_recommendation(
growth_stage=resolved_growth_stage,
forecasts=forecasts,
)
result.setdefault("source_metadata", {})
result["source_metadata"]["farm_metrics"] = (ai_snapshot or {}).get("source_metadata", {}).get("farm_metrics", {})
result["source_metadata"]["weather"] = {"source": "center_location_forecast", "policy": "center_location_latest_forecast"}
result = _validate_fertilization_response(result)
result["raw_response"] = raw
result["simulation_optimizer"] = optimized_result
@@ -6,6 +6,7 @@ from typing import Any, Literal
from pydantic import BaseModel, Field, ValidationError
from farm_data.services import build_ai_farm_snapshot
from rag.api_provider import get_chat_client
from rag.chat import (
_complete_audit_log,
@@ -124,7 +125,14 @@ class FertilizationPlanParserService:
"partial_plan": normalized_partial,
"required_core_fields": CORE_FIELDS,
"service": "fertilization_plan_parser",
"endpoint_policy": "parser_first",
}
if farm_uuid:
# Parser-first endpoint: farm context is optional enrichment only.
structured_context["farm_context_source_metadata"] = {
"source": "build_ai_farm_snapshot",
"optional": True,
}
rag_query = self._build_retrieval_query(
message=normalized_message,
+17 -10
View File
@@ -11,7 +11,10 @@ from django.db import transaction
from farm_data.models import SensorData
from farm_data.services import (
build_ai_farm_snapshot,
clone_snapshot_as_runtime_plant,
get_ai_snapshot_metric,
get_ai_snapshot_weather,
get_farm_plant_snapshot_by_name,
)
from irrigation.evapotranspiration import (
@@ -55,13 +58,15 @@ def _safe_float(value: Any, default: float = 0.0) -> float:
return default
def _sensor_metric(sensor: SensorData | None, metric: str) -> float | None:
if sensor is None or not isinstance(sensor.sensor_payload, dict):
return None
for payload in sensor.sensor_payload.values():
if isinstance(payload, dict) and payload.get(metric) is not None:
return _safe_float(payload.get(metric), default=0.0)
return None
def _aggregated_metric(ai_snapshot: dict[str, Any] | None, metric: str) -> float | None:
value = get_ai_snapshot_metric(ai_snapshot, metric)
return _safe_float(value, default=0.0) if value is not None else None
def _aggregated_metric_fallback(ai_snapshot: dict[str, Any] | None, metric: str) -> float | None:
"""Limited fallback for missing aggregated metrics only; raw payload is intentionally not consulted."""
return _aggregated_metric(ai_snapshot, metric)
def _coerce_list(value: Any) -> list[Any]:
@@ -275,9 +280,9 @@ def _build_irrigation_ui_payload(
crop_profile: dict[str, Any],
active_kc: float,
irrigation_method: IrrigationMethod | None,
sensor: SensorData | None,
ai_snapshot: dict[str, Any] | None,
) -> dict[str, Any]:
soil_moisture = _sensor_metric(sensor, "soil_moisture")
soil_moisture = _aggregated_metric_fallback(ai_snapshot, "soil_moisture")
plan = _normalize_plan(
llm_result,
optimizer_result,
@@ -380,6 +385,7 @@ def get_irrigation_recommendation(
.filter(farm_uuid=resolved_farm_uuid)
.first()
)
ai_snapshot = build_ai_farm_snapshot(resolved_farm_uuid)
irrigation_method = _resolve_irrigation_method(sensor, irrigation_method_name)
_persist_irrigation_method_on_farm(sensor, irrigation_method)
@@ -423,6 +429,7 @@ def get_irrigation_recommendation(
if plant is not None and forecasts:
optimized_result = _get_optimizer().optimize_irrigation(
sensor=sensor,
ai_snapshot=ai_snapshot,
plant=plant,
forecasts=forecasts,
daily_water_needs=daily_water_needs,
@@ -518,7 +525,7 @@ def get_irrigation_recommendation(
crop_profile,
active_kc,
irrigation_method,
sensor,
ai_snapshot,
)
result["raw_response"] = raw
result["simulation_optimizer"] = optimized_result
+8
View File
@@ -6,6 +6,7 @@ from typing import Any, Literal
from pydantic import BaseModel, Field, ValidationError
from farm_data.services import build_ai_farm_snapshot
from rag.api_provider import get_chat_client
from rag.chat import (
_complete_audit_log,
@@ -120,7 +121,14 @@ class IrrigationPlanParserService:
"partial_plan": normalized_partial,
"required_core_fields": CORE_FIELDS,
"service": "irrigation_plan_parser",
"endpoint_policy": "parser_first",
}
if farm_uuid:
# Parser-first endpoint: farm context is optional enrichment only.
structured_context["farm_context_source_metadata"] = {
"source": "build_ai_farm_snapshot",
"optional": True,
}
rag_query = self._build_retrieval_query(
message=normalized_message,
+4 -4
View File
@@ -7,7 +7,7 @@ import json
import logging
from typing import Any
from farm_data.services import get_farm_details
from farm_data.services import build_ai_farm_snapshot
from rag.api_provider import get_chat_client
from rag.chat import (
_build_content_parts,
@@ -106,7 +106,7 @@ def _clean_json(raw: str) -> dict[str, Any]:
def _load_farm_or_error(farm_uuid: str) -> dict[str, Any]:
farm_details = get_farm_details(farm_uuid)
farm_details = build_ai_farm_snapshot(farm_uuid)
if farm_details is None:
raise RAGServiceError(
error_code="farm_not_found",
@@ -134,8 +134,8 @@ def _build_service_client(cfg: RAGConfig):
def _weather_risk_summary(farm_details: dict[str, Any]) -> dict[str, Any]:
weather = farm_details.get("weather") or {}
soil = (farm_details.get("soil") or {}).get("resolved_metrics") or {}
weather = ((farm_details.get("weather") or {}).get("forecast") or {})
soil = (farm_details.get("farm_metrics") or {}).get("resolved_metrics") or {}
humidity = _safe_float(weather.get("humidity_mean"), 55.0)
temp = _safe_float(weather.get("temperature_mean"), 24.0)
rain = _safe_float(weather.get("precipitation"), 0.0)
+2 -2
View File
@@ -4,7 +4,7 @@ import json
import logging
from typing import Any
from farm_data.services import get_farm_details
from farm_data.services import build_ai_farm_snapshot
from rag.api_provider import get_chat_client
from rag.chat import (
_complete_audit_log,
@@ -73,7 +73,7 @@ def _clean_json(raw: str) -> dict[str, Any]:
def _load_farm_or_error(farm_uuid: str) -> dict[str, Any]:
farm_details = get_farm_details(farm_uuid)
farm_details = build_ai_farm_snapshot(farm_uuid)
if farm_details is None:
raise RAGServiceError(
error_code="farm_not_found",
+22 -2
View File
@@ -4,7 +4,7 @@ import json
import logging
from typing import Any
from farm_data.services import get_farm_details
from farm_data.services import build_ai_farm_snapshot, get_ai_snapshot_metric, get_ai_snapshot_weather
from rag.api_provider import get_chat_client
from rag.chat import (
_complete_audit_log,
@@ -71,7 +71,7 @@ def _clean_json(raw: str) -> dict[str, Any]:
def _load_farm_or_error(farm_uuid: str) -> dict[str, Any]:
farm_details = get_farm_details(farm_uuid)
farm_details = build_ai_farm_snapshot(farm_uuid)
if farm_details is None:
raise RAGServiceError(
error_code="farm_not_found",
@@ -158,6 +158,16 @@ def get_water_need_prediction_insight(
structured_context = {
"farm_uuid": farm_uuid,
"prediction_payload": prediction_payload,
"source_metadata": {
"agronomic_metrics": {
"source": "build_ai_farm_snapshot",
"policy": "cluster_block_farm_aggregated",
},
"weather": {
"source": "center_location_forecast",
"policy": "center_location_latest_forecast",
},
},
}
rag_context = build_rag_context(
query=user_query,
@@ -208,4 +218,14 @@ def get_water_need_prediction_insight(
parsed["source"] = "llm"
parsed["farm_uuid"] = farm_uuid
parsed["raw_response"] = raw
parsed["source_metadata"] = {
"agronomic_metrics": {
"source": "build_ai_farm_snapshot",
"policy": "cluster_block_farm_aggregated",
},
"weather": {
"source": "center_location_forecast",
"policy": "center_location_latest_forecast",
},
}
return parsed