UPDATE
This commit is contained in:
+45
-22
@@ -1,12 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
چت RAG برای API چت عمومی — استفاده مستقیم از داده مزرعه بدون retrieval/embedding.
|
چت RAG برای API چت عمومی — با ارسال کامل داده مزرعه و retrieval تکمیلی از KB.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .api_provider import get_chat_client
|
from .api_provider import get_chat_client
|
||||||
|
from .chunker import chunk_text
|
||||||
from .config import RAGConfig, ServiceConfig, get_service_config, load_rag_config
|
from .config import RAGConfig, ServiceConfig, get_service_config, load_rag_config
|
||||||
|
from .retrieve import search_with_texts
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -61,6 +63,17 @@ def _format_farm_context_from_details(farm_details: dict) -> str:
|
|||||||
return "[اطلاعات کامل مزرعه]\n" + serialized
|
return "[اطلاعات کامل مزرعه]\n" + serialized
|
||||||
|
|
||||||
|
|
||||||
|
def _load_farm_details_context(
|
||||||
|
sensor_uuid: str | None,
|
||||||
|
farm_details: dict | None = None,
|
||||||
|
) -> str:
|
||||||
|
if not sensor_uuid:
|
||||||
|
return ""
|
||||||
|
if farm_details is not None:
|
||||||
|
return _format_farm_context_from_details(farm_details)
|
||||||
|
return _format_farm_context(sensor_uuid)
|
||||||
|
|
||||||
|
|
||||||
def _build_system_prompt(
|
def _build_system_prompt(
|
||||||
service: ServiceConfig,
|
service: ServiceConfig,
|
||||||
query: str,
|
query: str,
|
||||||
@@ -72,8 +85,10 @@ def _build_system_prompt(
|
|||||||
if service.system_prompt:
|
if service.system_prompt:
|
||||||
system_parts.append(service.system_prompt)
|
system_parts.append(service.system_prompt)
|
||||||
system_parts.append(
|
system_parts.append(
|
||||||
"با استفاده از اطلاعات کامل مزرعه که در ادامه آمده به سوال کاربر پاسخ بده. "
|
"با استفاده از اطلاعات کامل مزرعه و اطلاعات بازیابیشده از پایگاه دانش که در ادامه آمده "
|
||||||
|
"به سوال کاربر پاسخ بده. "
|
||||||
"اگر دادهای در اطلاعات مزرعه وجود دارد، همان را مبنای پاسخ قرار بده و چیزی حدس نزن. "
|
"اگر دادهای در اطلاعات مزرعه وجود دارد، همان را مبنای پاسخ قرار بده و چیزی حدس نزن. "
|
||||||
|
"نتایج بازیابیشده از پایگاه دانش را برای تکمیل یا توضیح پاسخ استفاده کن. "
|
||||||
"اگر داده کافی نبود، این کمبود را شفاف بگو. "
|
"اگر داده کافی نبود، این کمبود را شفاف بگو. "
|
||||||
"پاسخ را به زبان کاربر بنویس."
|
"پاسخ را به زبان کاربر بنویس."
|
||||||
)
|
)
|
||||||
@@ -141,13 +156,15 @@ def build_rag_context(
|
|||||||
limit: int = 8,
|
limit: int = 8,
|
||||||
kb_name: str | None = None,
|
kb_name: str | None = None,
|
||||||
service_id: str | None = None,
|
service_id: str | None = None,
|
||||||
|
farm_details: dict | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
ساخت context برای سرویسهای توصیه با استفاده از RAG قدیمی.
|
ساخت context مشترک برای همه سرویسهای RAG.
|
||||||
این تابع برای سازگاری با irrigation/fertilization حفظ شده است.
|
شامل:
|
||||||
|
- اطلاعات کامل مزرعه از farm_data/services.py
|
||||||
|
- جستجوی KB بر اساس پیام کاربر
|
||||||
|
- جستجوی KB بر اساس chunk های کامل داده مزرعه
|
||||||
"""
|
"""
|
||||||
from .retrieve import search_with_query
|
|
||||||
from .user_data import build_user_soil_text, build_user_weather_text
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Building RAG context sensor_uuid=%s kb_name=%s limit=%s query_len=%s",
|
"Building RAG context sensor_uuid=%s kb_name=%s limit=%s query_len=%s",
|
||||||
@@ -161,20 +178,23 @@ def build_rag_context(
|
|||||||
service = get_service_config(service_id, cfg) if service_id else None
|
service = get_service_config(service_id, cfg) if service_id else None
|
||||||
include_user_embeddings = service.use_user_embeddings if service else True
|
include_user_embeddings = service.use_user_embeddings if service else True
|
||||||
resolved_kb_name = kb_name or (service.knowledge_base if service else None)
|
resolved_kb_name = kb_name or (service.knowledge_base if service else None)
|
||||||
|
farm_context = _load_farm_details_context(
|
||||||
|
sensor_uuid=sensor_uuid,
|
||||||
|
farm_details=farm_details,
|
||||||
|
)
|
||||||
|
|
||||||
if include_user_embeddings and sensor_uuid:
|
if farm_context:
|
||||||
user_soil = build_user_soil_text(sensor_uuid)
|
parts.append(farm_context)
|
||||||
if user_soil and user_soil.strip():
|
|
||||||
parts.append("[دادههای فعلی خاک شما]\n" + user_soil.strip())
|
|
||||||
|
|
||||||
weather_text = build_user_weather_text(sensor_uuid)
|
search_texts = [query]
|
||||||
if weather_text and weather_text.strip():
|
if farm_context:
|
||||||
parts.append("[پیشبینی هواشناسی]\n" + weather_text.strip())
|
search_texts.extend(chunk_text(farm_context, config=cfg))
|
||||||
|
|
||||||
results = search_with_query(
|
results = search_with_texts(
|
||||||
query,
|
search_texts,
|
||||||
sensor_uuid=sensor_uuid,
|
sensor_uuid=sensor_uuid,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
|
per_text_limit=3,
|
||||||
config=cfg,
|
config=cfg,
|
||||||
kb_name=resolved_kb_name,
|
kb_name=resolved_kb_name,
|
||||||
service_id=service_id,
|
service_id=service_id,
|
||||||
@@ -230,20 +250,23 @@ def chat_rag_stream(
|
|||||||
len(query or ""),
|
len(query or ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
if farm_details is None:
|
context = build_rag_context(
|
||||||
farm_context = _format_farm_context(farm_uuid)
|
query=query,
|
||||||
else:
|
sensor_uuid=farm_uuid,
|
||||||
farm_context = _format_farm_context_from_details(farm_details)
|
config=cfg,
|
||||||
|
service_id=service_id,
|
||||||
|
farm_details=farm_details,
|
||||||
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
"Loaded farm context for farm_uuid=%s context_len=%s",
|
"Loaded augmented context for farm_uuid=%s context_len=%s",
|
||||||
farm_uuid,
|
farm_uuid,
|
||||||
len(farm_context),
|
len(context),
|
||||||
)
|
)
|
||||||
|
|
||||||
if system_override is not None:
|
if system_override is not None:
|
||||||
system_prompt = system_override
|
system_prompt = system_override
|
||||||
else:
|
else:
|
||||||
system_prompt = _build_system_prompt(service, query, farm_context, cfg)
|
system_prompt = _build_system_prompt(service, query, context, cfg)
|
||||||
|
|
||||||
messages = [
|
messages = [
|
||||||
{"role": "system", "content": system_prompt},
|
{"role": "system", "content": system_prompt},
|
||||||
|
|||||||
+85
-16
@@ -2,10 +2,37 @@
|
|||||||
بازیابی RAG: embed کوئری و جستجو در vector store
|
بازیابی RAG: embed کوئری و جستجو در vector store
|
||||||
"""
|
"""
|
||||||
from .config import load_rag_config, RAGConfig, get_service_config
|
from .config import load_rag_config, RAGConfig, get_service_config
|
||||||
from .embedding import embed_single
|
from .embedding import embed_single, embed_texts
|
||||||
from .vector_store import QdrantVectorStore
|
from .vector_store import QdrantVectorStore
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_search_options(
|
||||||
|
sensor_uuid: str | None = None,
|
||||||
|
config: RAGConfig | None = None,
|
||||||
|
kb_name: str | None = None,
|
||||||
|
service_id: str | None = None,
|
||||||
|
use_user_embeddings: bool | None = None,
|
||||||
|
) -> tuple[RAGConfig, list[str], list[str]]:
|
||||||
|
cfg = config or load_rag_config()
|
||||||
|
service = get_service_config(service_id, cfg) if service_id else None
|
||||||
|
resolved_kb_name = kb_name or (service.knowledge_base if service else None)
|
||||||
|
include_user_embeddings = (
|
||||||
|
use_user_embeddings
|
||||||
|
if use_user_embeddings is not None
|
||||||
|
else (service.use_user_embeddings if service else True)
|
||||||
|
)
|
||||||
|
|
||||||
|
sensor_filters = ["__global__"]
|
||||||
|
if include_user_embeddings and sensor_uuid:
|
||||||
|
sensor_filters.insert(0, sensor_uuid)
|
||||||
|
|
||||||
|
kb_filters = [resolved_kb_name] if resolved_kb_name else []
|
||||||
|
if include_user_embeddings:
|
||||||
|
kb_filters.append("__all__")
|
||||||
|
|
||||||
|
return cfg, sensor_filters, kb_filters
|
||||||
|
|
||||||
|
|
||||||
def search_with_query(
|
def search_with_query(
|
||||||
query: str,
|
query: str,
|
||||||
sensor_uuid: str | None = None,
|
sensor_uuid: str | None = None,
|
||||||
@@ -28,23 +55,14 @@ def search_with_query(
|
|||||||
Returns:
|
Returns:
|
||||||
لیست نتایج با id, score, text, metadata
|
لیست نتایج با id, score, text, metadata
|
||||||
"""
|
"""
|
||||||
cfg = config or load_rag_config()
|
cfg, sensor_filters, kb_filters = _resolve_search_options(
|
||||||
service = get_service_config(service_id, cfg) if service_id else None
|
sensor_uuid=sensor_uuid,
|
||||||
resolved_kb_name = kb_name or (service.knowledge_base if service else None)
|
config=config,
|
||||||
include_user_embeddings = (
|
kb_name=kb_name,
|
||||||
use_user_embeddings
|
service_id=service_id,
|
||||||
if use_user_embeddings is not None
|
use_user_embeddings=use_user_embeddings,
|
||||||
else (service.use_user_embeddings if service else True)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
sensor_filters = ["__global__"]
|
|
||||||
if include_user_embeddings and sensor_uuid:
|
|
||||||
sensor_filters.insert(0, sensor_uuid)
|
|
||||||
|
|
||||||
kb_filters = [resolved_kb_name] if resolved_kb_name else []
|
|
||||||
if include_user_embeddings:
|
|
||||||
kb_filters.append("__all__")
|
|
||||||
|
|
||||||
query_vector = embed_single(query, config=cfg)
|
query_vector = embed_single(query, config=cfg)
|
||||||
store = QdrantVectorStore(config=cfg)
|
store = QdrantVectorStore(config=cfg)
|
||||||
return store.search(
|
return store.search(
|
||||||
@@ -54,3 +72,54 @@ def search_with_query(
|
|||||||
sensor_uuids=sensor_filters,
|
sensor_uuids=sensor_filters,
|
||||||
kb_names=kb_filters,
|
kb_names=kb_filters,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def search_with_texts(
|
||||||
|
texts: list[str],
|
||||||
|
sensor_uuid: str | None = None,
|
||||||
|
limit: int = 8,
|
||||||
|
per_text_limit: int = 3,
|
||||||
|
score_threshold: float | None = None,
|
||||||
|
config: RAGConfig | None = None,
|
||||||
|
kb_name: str | None = None,
|
||||||
|
service_id: str | None = None,
|
||||||
|
use_user_embeddings: bool | None = None,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""
|
||||||
|
چند متن را embed میکند و نتیجه جستجوها را به صورت dedupe شده برمیگرداند.
|
||||||
|
برای حالتی مناسب است که هم پیام کاربر و هم دادههای مزرعه را علیه KB جستجو کنیم.
|
||||||
|
"""
|
||||||
|
normalized_texts = [text.strip() for text in texts if text and text.strip()]
|
||||||
|
if not normalized_texts:
|
||||||
|
return []
|
||||||
|
|
||||||
|
cfg, sensor_filters, kb_filters = _resolve_search_options(
|
||||||
|
sensor_uuid=sensor_uuid,
|
||||||
|
config=config,
|
||||||
|
kb_name=kb_name,
|
||||||
|
service_id=service_id,
|
||||||
|
use_user_embeddings=use_user_embeddings,
|
||||||
|
)
|
||||||
|
|
||||||
|
store = QdrantVectorStore(config=cfg)
|
||||||
|
vectors = embed_texts(normalized_texts, config=cfg)
|
||||||
|
merged_results: dict[str, dict] = {}
|
||||||
|
|
||||||
|
for vector in vectors:
|
||||||
|
results = store.search(
|
||||||
|
query_vector=vector,
|
||||||
|
limit=per_text_limit,
|
||||||
|
score_threshold=score_threshold,
|
||||||
|
sensor_uuids=sensor_filters,
|
||||||
|
kb_names=kb_filters,
|
||||||
|
)
|
||||||
|
for item in results:
|
||||||
|
current = merged_results.get(item["id"])
|
||||||
|
if current is None or item["score"] > current["score"]:
|
||||||
|
merged_results[item["id"]] = item
|
||||||
|
|
||||||
|
return sorted(
|
||||||
|
merged_results.values(),
|
||||||
|
key=lambda item: item["score"],
|
||||||
|
reverse=True,
|
||||||
|
)[:limit]
|
||||||
|
|||||||
+47
-15
@@ -5,6 +5,8 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from irrigation.models import IrrigationMethod
|
||||||
from irrigation.evapotranspiration import calculate_forecast_water_needs, resolve_crop_profile, resolve_kc
|
from irrigation.evapotranspiration import calculate_forecast_water_needs, resolve_crop_profile, resolve_kc
|
||||||
from farm_data.models import SensorData
|
from farm_data.models import SensorData
|
||||||
from rag.api_provider import get_chat_client
|
from rag.api_provider import get_chat_client
|
||||||
@@ -42,6 +44,31 @@ DEFAULT_IRRIGATION_PROMPT = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_irrigation_method(
|
||||||
|
sensor: SensorData | None,
|
||||||
|
irrigation_method_name: str | None,
|
||||||
|
) -> IrrigationMethod | None:
|
||||||
|
if irrigation_method_name:
|
||||||
|
return IrrigationMethod.objects.filter(name=irrigation_method_name).first()
|
||||||
|
if sensor is not None:
|
||||||
|
return sensor.irrigation_method
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _persist_irrigation_method_on_farm(
|
||||||
|
sensor: SensorData | None,
|
||||||
|
irrigation_method: IrrigationMethod | None,
|
||||||
|
) -> None:
|
||||||
|
if sensor is None or irrigation_method is None:
|
||||||
|
return
|
||||||
|
if sensor.irrigation_method_id == irrigation_method.id:
|
||||||
|
return
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
sensor.irrigation_method = irrigation_method
|
||||||
|
sensor.save(update_fields=["irrigation_method", "updated_at"])
|
||||||
|
|
||||||
|
|
||||||
def get_irrigation_recommendation(
|
def get_irrigation_recommendation(
|
||||||
sensor_uuid: str,
|
sensor_uuid: str,
|
||||||
plant_name: str | None = None,
|
plant_name: str | None = None,
|
||||||
@@ -89,6 +116,9 @@ def get_irrigation_recommendation(
|
|||||||
.filter(farm_uuid=sensor_uuid)
|
.filter(farm_uuid=sensor_uuid)
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
irrigation_method = _resolve_irrigation_method(sensor, irrigation_method_name)
|
||||||
|
_persist_irrigation_method_on_farm(sensor, irrigation_method)
|
||||||
|
|
||||||
plant = None
|
plant = None
|
||||||
resolved_plant_name = plant_name
|
resolved_plant_name = plant_name
|
||||||
if sensor is not None and plant_name:
|
if sensor is not None and plant_name:
|
||||||
@@ -106,19 +136,11 @@ def get_irrigation_recommendation(
|
|||||||
WeatherForecast.objects.filter(location=sensor.center_location, forecast_date__isnull=False)
|
WeatherForecast.objects.filter(location=sensor.center_location, forecast_date__isnull=False)
|
||||||
.order_by("forecast_date")[:7]
|
.order_by("forecast_date")[:7]
|
||||||
)
|
)
|
||||||
efficiency_percent = None
|
efficiency_percent = (
|
||||||
resolved_irrigation_method_name = irrigation_method_name
|
getattr(irrigation_method, "water_efficiency_percent", None)
|
||||||
method = None
|
if irrigation_method
|
||||||
if irrigation_method_name:
|
else None
|
||||||
from irrigation.models import IrrigationMethod
|
)
|
||||||
|
|
||||||
method = IrrigationMethod.objects.filter(name=irrigation_method_name).first()
|
|
||||||
elif sensor is not None:
|
|
||||||
method = sensor.irrigation_method
|
|
||||||
if method is not None:
|
|
||||||
resolved_irrigation_method_name = method.name
|
|
||||||
|
|
||||||
efficiency_percent = getattr(method, "water_efficiency_percent", None) if method else None
|
|
||||||
daily_water_needs = calculate_forecast_water_needs(
|
daily_water_needs = calculate_forecast_water_needs(
|
||||||
forecasts=forecasts,
|
forecasts=forecasts,
|
||||||
latitude_deg=float(sensor.center_location.latitude),
|
latitude_deg=float(sensor.center_location.latitude),
|
||||||
@@ -132,8 +154,8 @@ def get_irrigation_recommendation(
|
|||||||
)
|
)
|
||||||
|
|
||||||
extra_parts: list[str] = []
|
extra_parts: list[str] = []
|
||||||
resolved_irrigation_method_name = irrigation_method_name or (
|
resolved_irrigation_method_name = (
|
||||||
sensor.irrigation_method.name if sensor is not None and sensor.irrigation_method else None
|
irrigation_method.name if irrigation_method is not None else None
|
||||||
)
|
)
|
||||||
if resolved_plant_name and growth_stage:
|
if resolved_plant_name and growth_stage:
|
||||||
plant_text = build_plant_text(resolved_plant_name, growth_stage)
|
plant_text = build_plant_text(resolved_plant_name, growth_stage)
|
||||||
@@ -222,6 +244,16 @@ def get_irrigation_recommendation(
|
|||||||
"crop_profile": crop_profile,
|
"crop_profile": crop_profile,
|
||||||
"active_kc": active_kc,
|
"active_kc": active_kc,
|
||||||
}
|
}
|
||||||
|
result["selected_irrigation_method"] = (
|
||||||
|
{
|
||||||
|
"id": irrigation_method.id,
|
||||||
|
"name": irrigation_method.name,
|
||||||
|
"category": irrigation_method.category,
|
||||||
|
"water_efficiency_percent": irrigation_method.water_efficiency_percent,
|
||||||
|
}
|
||||||
|
if irrigation_method is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
_complete_audit_log(
|
_complete_audit_log(
|
||||||
audit_log,
|
audit_log,
|
||||||
json.dumps(result, ensure_ascii=False, default=str),
|
json.dumps(result, ensure_ascii=False, default=str),
|
||||||
|
|||||||
@@ -2,51 +2,54 @@ from unittest.mock import patch
|
|||||||
|
|
||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
|
|
||||||
from rag.chat import build_chat_context
|
from rag.chat import build_rag_context
|
||||||
|
|
||||||
|
|
||||||
class ChatContextTests(SimpleTestCase):
|
class ChatContextTests(SimpleTestCase):
|
||||||
@patch("rag.chat.search_with_query")
|
@patch("rag.chat.search_with_texts")
|
||||||
@patch("rag.chat._rank_text_chunks_by_query")
|
|
||||||
@patch("rag.chat.chunk_text")
|
@patch("rag.chat.chunk_text")
|
||||||
def test_build_chat_context_combines_farm_and_kb_context(
|
def test_build_rag_context_includes_full_farm_and_kb_results(
|
||||||
self,
|
self,
|
||||||
mock_chunk_text,
|
mock_chunk_text,
|
||||||
mock_rank_text_chunks_by_query,
|
mock_search_with_texts,
|
||||||
mock_search_with_query,
|
|
||||||
):
|
):
|
||||||
mock_chunk_text.return_value = ["chunk-a", "chunk-b"]
|
mock_chunk_text.return_value = ["farm chunk 1", "farm chunk 2"]
|
||||||
mock_rank_text_chunks_by_query.return_value = ["chunk-b"]
|
mock_search_with_texts.return_value = [
|
||||||
mock_search_with_query.return_value = [
|
{"id": "kb-1", "score": 0.8, "text": "kb text 1", "metadata": {}},
|
||||||
{"text": "kb text 1"},
|
{"id": "kb-2", "score": 0.7, "text": "kb text 2", "metadata": {}},
|
||||||
{"text": "kb text 2"},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
context = build_chat_context(
|
context = build_rag_context(
|
||||||
query="وضعیت مزرعه چطور است؟",
|
query="وضعیت مزرعه چطور است؟",
|
||||||
farm_uuid="farm-123",
|
sensor_uuid="farm-123",
|
||||||
|
service_id="chat",
|
||||||
farm_details={"sensor_payload": {"sensor-7-1": {"soil_moisture": 30}}},
|
farm_details={"sensor_payload": {"sensor-7-1": {"soil_moisture": 30}}},
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertIn("[بخشهای مرتبط بازیابیشده از اطلاعات مزرعه]", context)
|
self.assertIn("[اطلاعات کامل مزرعه]", context)
|
||||||
self.assertIn("chunk-b", context)
|
self.assertIn("soil_moisture", context)
|
||||||
self.assertIn("[اطلاعات بازیابیشده از پایگاه دانش]", context)
|
self.assertIn("[متنهای مرجع]", context)
|
||||||
self.assertIn("kb text 1", context)
|
self.assertIn("kb text 1", context)
|
||||||
self.assertIn("kb text 2", context)
|
self.assertIn("kb text 2", context)
|
||||||
|
mock_search_with_texts.assert_called_once()
|
||||||
|
sent_texts = mock_search_with_texts.call_args.kwargs["texts"]
|
||||||
|
self.assertEqual(sent_texts[0], "وضعیت مزرعه چطور است؟")
|
||||||
|
self.assertIn("farm chunk 1", sent_texts)
|
||||||
|
self.assertIn("farm chunk 2", sent_texts)
|
||||||
|
|
||||||
@patch("rag.chat.search_with_query", return_value=[])
|
@patch("rag.chat.search_with_texts", return_value=[])
|
||||||
@patch("rag.chat._rank_text_chunks_by_query", return_value=[])
|
|
||||||
@patch("rag.chat.chunk_text", return_value=["farm chunk"])
|
@patch("rag.chat.chunk_text", return_value=["farm chunk"])
|
||||||
def test_build_chat_context_falls_back_to_full_farm_context(
|
def test_build_rag_context_returns_full_farm_when_kb_empty(
|
||||||
self,
|
self,
|
||||||
_mock_chunk_text,
|
_mock_chunk_text,
|
||||||
_mock_rank_text_chunks_by_query,
|
_mock_search_with_texts,
|
||||||
_mock_search_with_query,
|
|
||||||
):
|
):
|
||||||
context = build_chat_context(
|
context = build_rag_context(
|
||||||
query="رطوبت چقدر است؟",
|
query="رطوبت چقدر است؟",
|
||||||
farm_uuid="farm-123",
|
sensor_uuid="farm-123",
|
||||||
|
service_id="chat",
|
||||||
farm_details={"sensor_payload": {"sensor-7-1": {"soil_moisture": 30}}},
|
farm_details={"sensor_payload": {"sensor-7-1": {"soil_moisture": 30}}},
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(context, "")
|
self.assertIn("[اطلاعات کامل مزرعه]", context)
|
||||||
|
self.assertIn("soil_moisture", context)
|
||||||
|
|||||||
@@ -68,6 +68,46 @@ class RecommendationServiceDefaultsTests(TestCase):
|
|||||||
mock_build_rag_context.assert_called_once()
|
mock_build_rag_context.assert_called_once()
|
||||||
mock_build_plant_text.assert_called_once_with("گوجهفرنگی", "میوهدهی")
|
mock_build_plant_text.assert_called_once_with("گوجهفرنگی", "میوهدهی")
|
||||||
mock_build_irrigation_method_text.assert_called_once_with("آبیاری قطرهای")
|
mock_build_irrigation_method_text.assert_called_once_with("آبیاری قطرهای")
|
||||||
|
self.assertEqual(
|
||||||
|
result["selected_irrigation_method"]["name"],
|
||||||
|
"آبیاری قطرهای",
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("rag.services.irrigation.calculate_forecast_water_needs", return_value=[])
|
||||||
|
@patch("rag.services.irrigation.resolve_kc", return_value=0.9)
|
||||||
|
@patch("rag.services.irrigation.resolve_crop_profile", return_value={})
|
||||||
|
@patch("rag.services.irrigation.build_irrigation_method_text", return_value="method text")
|
||||||
|
@patch("rag.services.irrigation.build_plant_text", return_value="plant text")
|
||||||
|
@patch("rag.services.irrigation.build_rag_context", return_value="")
|
||||||
|
@patch("rag.services.irrigation.get_chat_client")
|
||||||
|
def test_irrigation_recommendation_persists_selected_method_on_farm(
|
||||||
|
self,
|
||||||
|
mock_get_chat_client,
|
||||||
|
_mock_build_rag_context,
|
||||||
|
_mock_build_plant_text,
|
||||||
|
mock_build_irrigation_method_text,
|
||||||
|
_mock_resolve_crop_profile,
|
||||||
|
_mock_resolve_kc,
|
||||||
|
_mock_calculate_forecast_water_needs,
|
||||||
|
):
|
||||||
|
sprinkler = IrrigationMethod.objects.create(name="بارانی")
|
||||||
|
self.farm.irrigation_method = None
|
||||||
|
self.farm.save(update_fields=["irrigation_method", "updated_at"])
|
||||||
|
|
||||||
|
mock_response = Mock()
|
||||||
|
mock_response.choices = [Mock(message=Mock(content='{"plan": {"frequencyPerWeek": 4}}'))]
|
||||||
|
mock_get_chat_client.return_value.chat.completions.create.return_value = mock_response
|
||||||
|
|
||||||
|
result = get_irrigation_recommendation(
|
||||||
|
sensor_uuid=str(self.farm_uuid),
|
||||||
|
growth_stage="میوهدهی",
|
||||||
|
irrigation_method_name="بارانی",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.farm.refresh_from_db()
|
||||||
|
self.assertEqual(self.farm.irrigation_method_id, sprinkler.id)
|
||||||
|
self.assertEqual(result["selected_irrigation_method"]["id"], sprinkler.id)
|
||||||
|
mock_build_irrigation_method_text.assert_called_once_with("بارانی")
|
||||||
|
|
||||||
@patch("rag.services.fertilization.build_plant_text", return_value="plant text")
|
@patch("rag.services.fertilization.build_plant_text", return_value="plant text")
|
||||||
@patch("rag.services.fertilization.build_rag_context", return_value="")
|
@patch("rag.services.fertilization.build_rag_context", return_value="")
|
||||||
|
|||||||
+2
-2
@@ -8,6 +8,6 @@ from .views import (
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("chat/", ChatView.as_view()),
|
path("chat/", ChatView.as_view()),
|
||||||
path("recommend/irrigation/", IrrigationRecommendationView.as_view(), name="recommend-irrigation"),
|
# path("recommend/irrigation/", IrrigationRecommendationView.as_view(), name="recommend-irrigation"),
|
||||||
path("recommend/fertilization/", FertilizationRecommendationView.as_view(), name="recommend-fertilization"),
|
# path("recommend/fertilization/", FertilizationRecommendationView.as_view(), name="recommend-fertilization"),
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user