This commit is contained in:
2026-04-24 01:23:56 +03:30
parent 5acee1fa2c
commit 31f4bf5d38
16 changed files with 518 additions and 192 deletions
+7 -3
View File
@@ -1,21 +1,25 @@
# تنظیمات RAG برای پایگاه دانش CropLogic # تنظیمات RAG برای پایگاه دانش CropLogic
embedding: embedding:
provider: "gapgpt" # gapgpt یا avalai provider: "arvancloud" # gapgpt یا avalai یا arvancloud
model: "text-embedding-3-small" model: "Bge-m3-smka5"
base_url: "https://api.gapgpt.app/v1" base_url: "https://api.gapgpt.app/v1"
api_key_env: "GAPGPT_API_KEY" api_key_env: "GAPGPT_API_KEY"
batch_size: 32 batch_size: 32
# تنظیمات Avalai (برای fallback) # تنظیمات Avalai (برای fallback)
avalai_base_url: "https://api.avalai.ir/v1" avalai_base_url: "https://api.avalai.ir/v1"
avalai_api_key_env: "AVALAI_API_KEY" 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 به‌عنوان vector store
qdrant: qdrant:
host: "localhost" # یا qdrant در Docker host: "localhost" # یا qdrant در Docker
port: 6333 port: 6333
collection_name: "croplogic_kb" collection_name: "croplogic_kb"
vector_size: 1536 # متناسب با text-embedding-3-small vector_size: 1024 # متناسب با BGE-M3
chunking: chunking:
max_chunk_tokens: 500 max_chunk_tokens: 500
+38 -6
View File
@@ -1,7 +1,39 @@
# فایل لحن / سبک پاسخ‌های RAG **قالب خروجی (Output Format):**
شما موظف هستید پاسخ خود را **فقط و فقط** در قالب یک شیء JSON معتبر برگردانید. هیچ متن اضافی قبل یا بعد از JSON نباید وجود داشته باشد. ساختار JSON باید به شکل زیر باشد:
لحن و سبک پاسخ‌ها: {
- سطح: دوستانه و تخصصی؛ با کشاورز به زبان ساده و علمی صحبت کن. "content": "متن کلی و دوستانه پاسخ به کشاورز",
- واژگان: از اصطلاحات رایج کشاورزی و خاک‌شناسی استفاده کن، در صورت نیاز معادل فارسی بیاور. "sections": [
- طول: پاسخ‌ها مختصر و کاربردی؛ در صورت لزوم با بولت یا شماره ساختاربندی کن. // نکته بسیار مهم: این آرایه می‌تواند شامل یک، دو یا هر سه نوع بخش زیر باشد. هر بخش را **فقط و فقط در صورتی** اضافه کن که برای پاسخ به سوال کشاورز ضروری و مرتبط باشد:
- هشدار: اگر موضوع ایمنی یا سلامتی گیاه/خاک باشد، صریحاً هشدار بده.
// ۱. فقط در صورت نیاز به ارائه توصیه یا برنامه اجرایی:
{
"type": "recommendation",
"title": "عنوان توصیه یا برنامه (مثلاً برنامه آبیاری یا یک توصیه کلی)",
"icon": "نام آیکون مناسب مثل droplet یا sprout",
"content": "در صورتی که توصیه فقط یک متن ساده است، آن را اینجا بنویسید (اختیاری)",
"frequency": "میزان تکرار (اختیاری - فقط اگر برنامه دقیق است)",
"amount": "مقدار مصرف (اختیاری - فقط اگر برنامه دقیق است)",
"timing": "زمان‌بندی مناسب (اختیاری - فقط اگر برنامه دقیق است)",
"expandableExplanation": "توضیح علمی و ساده برای این توصیه (اختیاری)"
},
// ۲. فقط در صورت وجود نکات مهم که باید لیست شوند:
{
"type": "list",
"title": "عنوان لیست",
"icon": "نام آیکون مناسب",
"items": ["نکته اول", "نکته دوم"]
},
// ۳. فقط در صورت وجود خطر برای گیاه/خاک و نیاز به هشدار:
{
"type": "warning",
"title": "عنوان هشدار",
"icon": "نام آیکون مثل alert-triangle",
"content": "متن صریح و هشداردهنده در مورد خطر موجود"
}
]
}
**قانون مهم:** در بخش `recommendation`، اگر توصیه شما صرفاً یک متن ساده است، فقط از فیلدهای `title`، `icon` و `content` استفاده کنید. اما اگر یک برنامه دقیق است، می‌توانید از فیلدهای `frequency`، `amount` و `timing` استفاده کنید. فیلدهای خالی یا نامرتبط را از JSON حذف نکنید، بلکه مقدار آن‌ها را null قرار دهید یا به کلی از شیء حذف کنید (حذف کردن فیلدهای غیرضروری ترجیح داده می‌شود).
+1 -1
View File
@@ -24,7 +24,7 @@ services:
PMA_PORT: 3306 PMA_PORT: 3306
UPLOAD_LIMIT: 64M UPLOAD_LIMIT: 64M
ports: ports:
- "8082:80" - "8083:80"
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
+122
View File
@@ -0,0 +1,122 @@
2026-04-06 11:26:32,124 [INFO] django.utils.autoreload: /app/location_data/urls.py changed, reloading.
2026-04-06 11:26:34,398 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 11:39:14,251 [INFO] django.utils.autoreload: /app/sensor_data/urls.py changed, reloading.
2026-04-06 11:39:16,822 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 11:42:41,500 [INFO] django.utils.autoreload: /app/sensor_data/views.py changed, reloading.
2026-04-06 11:42:43,947 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 11:43:26,823 [INFO] django.utils.autoreload: /app/sensor_data/models.py changed, reloading.
2026-04-06 11:43:29,150 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 11:45:54,258 [INFO] django.utils.autoreload: /app/sensor_data/apps.py changed, reloading.
2026-04-06 11:45:56,525 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 11:46:06,483 [INFO] django.utils.autoreload: /app/sensor_data/models.py changed, reloading.
2026-04-06 11:46:09,070 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 11:46:11,650 [INFO] django.utils.autoreload: /app/location_data/models.py changed, reloading.
2026-04-06 11:46:14,185 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 12:18:40,264 [INFO] django.utils.autoreload: /app/rag/user_data.py changed, reloading.
2026-04-06 12:18:41,538 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 12:24:14,956 [INFO] django.utils.autoreload: /app/location_data/models.py changed, reloading.
2026-04-06 12:24:16,211 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 12:24:42,989 [INFO] django.utils.autoreload: /app/sensor_data/models.py changed, reloading.
2026-04-06 12:24:44,253 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 12:25:21,172 [INFO] django.utils.autoreload: /app/sensor_data/serializers.py changed, reloading.
2026-04-06 12:25:22,430 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 12:27:52,025 [INFO] django.utils.autoreload: /app/sensor_data/views.py changed, reloading.
2026-04-06 12:27:53,320 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 12:29:13,931 [INFO] django.utils.autoreload: /app/dashboard_data/context.py changed, reloading.
2026-04-06 12:29:15,202 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 12:29:52,134 [INFO] django.utils.autoreload: /app/dashboard_data/services.py changed, reloading.
2026-04-06 12:29:53,502 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 12:30:32,458 [INFO] django.utils.autoreload: /app/dashboard_data/cards/soil_moisture_heatmap.py changed, reloading.
2026-04-06 12:30:33,743 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 12:31:21,793 [INFO] django.utils.autoreload: /app/rag/user_data.py changed, reloading.
2026-04-06 12:31:23,054 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 12:32:42,612 [INFO] django.utils.autoreload: /app/sensor_data/admin.py changed, reloading.
2026-04-06 12:32:43,862 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 12:37:47,634 [INFO] django.utils.autoreload: /app/sensor_data/models.py changed, reloading.
2026-04-06 12:37:48,919 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 12:38:37,993 [INFO] django.utils.autoreload: /app/sensor_data/serializers.py changed, reloading.
2026-04-06 12:38:39,289 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 12:39:26,803 [INFO] django.server: "GET /api/docs/ HTTP/1.1" 200 4633
2026-04-06 12:39:26,837 [INFO] django.server: "GET /static/drf_spectacular_sidecar/swagger-ui-dist/swagger-ui.css HTTP/1.1" 304 0
2026-04-06 12:39:26,839 [INFO] django.server: "GET /static/drf_spectacular_sidecar/swagger-ui-dist/swagger-ui-bundle.js HTTP/1.1" 304 0
2026-04-06 12:39:26,840 [INFO] django.server: "GET /static/drf_spectacular_sidecar/swagger-ui-dist/swagger-ui-standalone-preset.js HTTP/1.1" 304 0
2026-04-06 12:39:27,022 [INFO] django.server: "GET /api/schema/ HTTP/1.1" 200 146171
2026-04-06 12:39:27,033 [INFO] django.server: "GET /static/drf_spectacular_sidecar/swagger-ui-dist/favicon-32x32.png HTTP/1.1" 304 0
2026-04-06 12:39:31,562 [INFO] django.utils.autoreload: /app/sensor_data/views.py changed, reloading.
2026-04-06 12:39:32,855 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 12:40:00,782 [INFO] django.utils.autoreload: /app/sensor_data/admin.py changed, reloading.
2026-04-06 12:40:02,109 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 12:44:16,444 [INFO] django.utils.autoreload: /app/sensor_data/serializers.py changed, reloading.
2026-04-06 12:44:17,780 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 12:45:00,763 [INFO] django.utils.autoreload: /app/sensor_data/views.py changed, reloading.
2026-04-06 12:45:02,047 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 12:45:10,529 [INFO] django.utils.autoreload: /app/sensor_data/urls.py changed, reloading.
2026-04-06 12:45:11,817 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 12:51:28,201 [INFO] django.utils.autoreload: /app/config/urls.py changed, reloading.
2026-04-06 12:51:29,679 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 12:54:04,072 [INFO] django.utils.autoreload: /app/dashboard_data/cards/soil_moisture_heatmap.py changed, reloading.
2026-04-06 12:54:05,375 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 19:42:33,233 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 19:45:04,902 [INFO] django.utils.autoreload: /app/farm_data/models.py changed, reloading.
2026-04-06 19:45:07,113 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 19:49:48,250 [INFO] django.utils.autoreload: /app/location_data/models.py changed, reloading.
2026-04-06 19:49:50,545 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 19:51:47,708 [INFO] django.server: "GET /api/docs/ HTTP/1.1" 200 4633
2026-04-06 19:51:48,041 [INFO] django.server: "GET /api/schema/ HTTP/1.1" 200 151490
2026-04-06 19:52:16,066 [ERROR] django.request: Internal Server Error: /api/farm-data/11111111-1111-1111-1111-111111111111/detail/
Traceback (most recent call last):
File "/usr/local/lib/python3.10/site-packages/django/core/handlers/exception.py", line 55, in inner
response = get_response(request)
File "/usr/local/lib/python3.10/site-packages/django/core/handlers/base.py", line 197, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/usr/local/lib/python3.10/site-packages/django/views/decorators/csrf.py", line 65, in _view_wrapper
return view_func(request, *args, **kwargs)
File "/usr/local/lib/python3.10/site-packages/django/views/generic/base.py", line 105, in view
return self.dispatch(request, *args, **kwargs)
File "/usr/local/lib/python3.10/site-packages/rest_framework/views.py", line 509, in dispatch
response = self.handle_exception(exc)
File "/usr/local/lib/python3.10/site-packages/rest_framework/views.py", line 469, in handle_exception
self.raise_uncaught_exception(exc)
File "/usr/local/lib/python3.10/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
raise exc
File "/usr/local/lib/python3.10/site-packages/rest_framework/views.py", line 506, in dispatch
response = handler(request, *args, **kwargs)
File "/app/farm_data/views.py", line 257, in get
data = get_farm_details(str(farm_uuid))
File "/app/farm_data/services.py", line 47, in get_farm_details
"ideal_sensor_profile": center_location.ideal_sensor_profile,
AttributeError: 'SoilLocation' object has no attribute 'ideal_sensor_profile'
2026-04-06 19:52:16,072 [ERROR] django.server: "GET /api/farm-data/11111111-1111-1111-1111-111111111111/detail/ HTTP/1.1" 500 18299
2026-04-06 19:53:58,048 [INFO] django.utils.autoreload: /app/farm_data/services.py changed, reloading.
2026-04-06 19:54:00,346 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 19:54:30,811 [INFO] django.utils.autoreload: /app/dashboard_data/cards/sensor_radar_chart.py changed, reloading.
2026-04-06 19:54:33,051 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 19:56:59,327 [INFO] django.utils.autoreload: /app/farm_data/serializers.py changed, reloading.
2026-04-06 19:57:01,484 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 19:57:23,727 [INFO] django.utils.autoreload: /app/farm_data/services.py changed, reloading.
2026-04-06 19:57:25,926 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 19:57:52,204 [INFO] django.utils.autoreload: /app/farm_data/views.py changed, reloading.
2026-04-06 19:57:54,397 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 19:59:14,870 [INFO] django.server: "GET /api/docs/ HTTP/1.1" 200 4633
2026-04-06 19:59:15,305 [INFO] django.server: "GET /api/schema/ HTTP/1.1" 200 153398
2026-04-06 19:59:38,242 [INFO] django.utils.autoreload: /app/farm_data/services.py changed, reloading.
2026-04-06 19:59:40,459 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 20:01:11,430 [INFO] django.utils.autoreload: /app/farm_data/services.py changed, reloading.
2026-04-06 20:01:13,631 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 20:05:55,681 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 20:06:01,544 [INFO] django.server: "GET /api/docs/ HTTP/1.1" 200 4633
2026-04-06 20:06:01,906 [INFO] django.server: "GET /api/schema/ HTTP/1.1" 200 153398
2026-04-06 20:06:22,223 [INFO] django.server: "GET /api/farm-data/11111111-1111-1111-1111-111111111111/detail/ HTTP/1.1" 200 2088
2026-04-06 20:09:19,767 [INFO] django.utils.autoreload: /app/farm_data/serializers.py changed, reloading.
2026-04-06 20:09:21,982 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 20:09:27,799 [INFO] django.utils.autoreload: /app/farm_data/services.py changed, reloading.
2026-04-06 20:09:29,995 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 20:09:45,822 [INFO] django.server: "GET /api/farm-data/11111111-1111-1111-1111-111111111111/detail/ HTTP/1.1" 200 3125
2026-04-06 20:10:27,291 [INFO] django.server: "POST /api/farm-data/ HTTP/1.1" 201 407
2026-04-06 20:11:07,920 [INFO] django.server: "GET /api/farm-data/550e8400-e29b-41d4-a716-446655440000/detail/ HTTP/1.1" 200 919
2026-04-06 20:26:57,235 [INFO] django.utils.autoreload: /app/location_data/tasks.py changed, reloading.
2026-04-06 20:26:59,781 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 20:27:32,325 [INFO] django.utils.autoreload: /app/farm_data/views.py changed, reloading.
2026-04-06 20:27:34,615 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-06 21:09:58,465 [ERROR] django.request: Bad Gateway: /api/farm-data/
2026-04-06 21:09:58,465 [ERROR] django.server: "POST /api/farm-data/ HTTP/1.1" 502 654
+1
View File
@@ -0,0 +1 @@
2026-04-07 07:48:04,983 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
+3
View File
@@ -0,0 +1,3 @@
2026-04-08 16:22:28,505 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-08 16:23:17,551 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
2026-04-08 16:25:19,939 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
+2 -27
View File
@@ -1,30 +1,5 @@
""" """
ماژول RAG — پایگاه دانش CropLogic ماژول RAG — برای جلوگیری از AppRegistryNotReady این فایل import سنگین انجام نمی‌دهد.
فاز یک: Qdrant به‌عنوان vector store
""" """
from .chat import chat_rag_stream __all__: list[str] = []
from .chunker import chunk_text, chunk_texts
from .client import get_qdrant_client
from .config import load_rag_config
from .embedding import embed_single, embed_texts
from .ingest import ingest, load_sources
from .retrieve import search_with_query
from .user_data import build_user_soil_text, load_user_sources
from .vector_store import QdrantVectorStore
__all__ = [
"chat_rag_stream",
"chunk_text",
"chunk_texts",
"embed_single",
"embed_texts",
"get_qdrant_client",
"ingest",
"load_rag_config",
"load_sources",
"load_user_sources",
"build_user_soil_text",
"QdrantVectorStore",
"search_with_query",
]
+21 -8
View File
@@ -1,7 +1,5 @@
""" """
Adapter Pattern برای API providers — سوئیچ بین GapGPT و Avalai Adapter Pattern برای API providers — سوئیچ بین GapGPT، Avalai و ArvanCloud AI.
تنظیمات فعلی: GapGPT به‌عنوان provider اصلی
Avalai به‌عنوان fallback نگه داشته شده.
""" """
import logging import logging
import os import os
@@ -20,22 +18,37 @@ def _mask_secret(value: str | None) -> str:
return "****" return "****"
return f"{value[:4]}...{value[-4:]}" return f"{value[:4]}...{value[-4:]}"
def _get_env_or_value(env_var: str | None, direct_value: str | None) -> str | None:
if env_var:
return os.environ.get(env_var) or direct_value
return direct_value
def get_embedding_client(config: RAGConfig | None = None) -> OpenAI: def get_embedding_client(config: RAGConfig | None = None) -> OpenAI:
""" """
ساخت کلاینت OpenAI برای Embedding بر اساس provider فعال. ساخت کلاینت OpenAI برای Embedding بر اساس provider فعال.
provider از config.embedding.provider خوانده می‌شود: "gapgpt" یا "avalai" provider از config.embedding.provider خوانده می‌شود.
""" """
cfg = config or load_rag_config() cfg = config or load_rag_config()
emb = cfg.embedding emb = cfg.embedding
logger.info("embedding provider=%s", emb.provider) provider = emb.provider or "gapgpt"
logger.info("embedding provider=%s", provider)
if emb.provider == "avalai": if provider == "avalai":
env_var = emb.avalai_api_key_env or emb.api_key_env or "AVALAI_API_KEY" env_var = emb.avalai_api_key_env or emb.api_key_env or "AVALAI_API_KEY"
api_key = os.environ.get(env_var) api_key = _get_env_or_value(env_var, emb.avalai_api_key or emb.api_key)
base_url = emb.avalai_base_url or emb.base_url or "https://api.avalai.ir/v1" base_url = emb.avalai_base_url or emb.base_url or "https://api.avalai.ir/v1"
elif provider == "arvancloud":
env_var = emb.arvancloud_api_key_env or "ARVANCLOUD_EMBEDDING_API_KEY"
api_key = _get_env_or_value(env_var, emb.arvancloud_api_key)
base_url = (
emb.arvancloud_base_url
or "https://arvancloudai.ir/gateway/models/Bge-m3/rBA2PgcTC2sfhXwamupI4NvQ8crddUGTYXOsuKVye91PoNuGhbRgpHHNY8sMHBVQWWerZSAi4a0AijUL6YBqY9EW-Y1LhW_0ec6Mxr85GQy41lXiV6M8Od4mvLIeDF-wLRUHIervod0O5ZqGj2MOX8z1zdUpXkCrIS2uDjHlfHBZofledZjsOVDmFZU7IYfvkA__ljQqNeKXSFgpwUR7SmsbRUXGTDB2moLdeRq9zBpQIw/v1"
)
else: else:
env_var = emb.api_key_env or "GAPGPT_API_KEY" env_var = emb.api_key_env or "GAPGPT_API_KEY"
api_key = os.environ.get(env_var) api_key = _get_env_or_value(env_var, emb.api_key)
base_url = emb.base_url or "https://api.gapgpt.app/v1" base_url = emb.base_url or "https://api.gapgpt.app/v1"
logger.info( logger.info(
"embedding base_url=%s api_key=%s", "embedding base_url=%s api_key=%s",
+173 -106
View File
@@ -1,13 +1,12 @@
""" """
چت RAG با استریم — استفاده از دیتای embed شده کاربر و Adapter API (GapGPT / Avalai) چت RAG برای API چت عمومی — استفاده مستقیم از داده مزرعه بدون retrieval/embedding.
""" """
import json
import logging import logging
from pathlib import Path from pathlib import Path
from .config import load_rag_config, RAGConfig, get_service_config, ServiceConfig
from .api_provider import get_chat_client from .api_provider import get_chat_client
from .retrieve import search_with_query from .config import RAGConfig, ServiceConfig, get_service_config, load_rag_config
from .user_data import build_user_soil_text, build_user_weather_text
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -19,30 +18,12 @@ def _load_tone(config: RAGConfig | None) -> str:
chat_kb = cfg.knowledge_bases.get("chat") chat_kb = cfg.knowledge_bases.get("chat")
if chat_kb: if chat_kb:
tone_path = base / chat_kb.tone_file tone_path = base / chat_kb.tone_file
logger.debug("Loading default tone from path=%s", tone_path)
if tone_path.exists(): if tone_path.exists():
logger.debug("Default tone file found: %s", tone_path)
return tone_path.read_text(encoding="utf-8").strip() return tone_path.read_text(encoding="utf-8").strip()
logger.warning("Default tone file not found: %s", tone_path) logger.warning("Default tone file not found: %s", tone_path)
return "" return ""
def _load_kb_tone(kb_name: str, config: RAGConfig | None = None) -> str:
"""بارگذاری فایل لحن مخصوص یک پایگاه دانش."""
cfg = config or load_rag_config()
kb_cfg = cfg.knowledge_bases.get(kb_name)
if not kb_cfg:
return ""
base = Path(__file__).resolve().parent.parent
tone_path = base / kb_cfg.tone_file
logger.debug("Loading kb tone for kb=%s path=%s", kb_name, tone_path)
if tone_path.exists():
logger.debug("KB tone file found for kb=%s", kb_name)
return tone_path.read_text(encoding="utf-8").strip()
logger.warning("KB tone file not found for kb=%s path=%s", kb_name, tone_path)
return ""
def _load_service_tone(service: ServiceConfig, config: RAGConfig | None = None) -> str: def _load_service_tone(service: ServiceConfig, config: RAGConfig | None = None) -> str:
cfg = config or load_rag_config() cfg = config or load_rag_config()
if service.tone_file: if service.tone_file:
@@ -50,21 +31,84 @@ def _load_service_tone(service: ServiceConfig, config: RAGConfig | None = None)
tone_path = base / service.tone_file tone_path = base / service.tone_file
if tone_path.exists(): if tone_path.exists():
return tone_path.read_text(encoding="utf-8").strip() return tone_path.read_text(encoding="utf-8").strip()
return _load_kb_tone(service.knowledge_base, cfg) logger.warning("Service tone file not found: %s", tone_path)
return _load_tone(cfg)
def _detect_kb_intent(query: str) -> str: def _format_farm_context(farm_uuid: str) -> str:
"""تشخیص ساده نوع پایگاه دانش مورد نیاز از روی متن سوال.""" from farm_data.services import get_farm_details
q = query.lower()
irrigation_keywords = {"آبیاری", "آب", "رطوبت", "irrigation", "water", "et0", "بارش", "خشکی"} farm_details = get_farm_details(farm_uuid)
fertilization_keywords = {"کود", "کودهی", "fertiliz", "npk", "ازت", "فسفر", "پتاسیم", "nitrogen", "phosphorus", "potassium"} if not farm_details:
if any(kw in q for kw in irrigation_keywords): raise ValueError("farm_uuid نامعتبر است یا اطلاعات مزرعه پیدا نشد.")
return "irrigation"
if any(kw in q for kw in fertilization_keywords): serialized = json.dumps(
logger.info("Detected KB intent=fertilization") farm_details,
return "fertilization" ensure_ascii=False,
logger.info("Detected KB intent=chat") indent=2,
return "chat" default=str,
)
return "[اطلاعات کامل مزرعه]\n" + serialized
def _format_farm_context_from_details(farm_details: dict) -> str:
serialized = json.dumps(
farm_details,
ensure_ascii=False,
indent=2,
default=str,
)
return "[اطلاعات کامل مزرعه]\n" + serialized
def _build_system_prompt(
service: ServiceConfig,
query: str,
farm_context: str,
config: RAGConfig | None = None,
) -> str:
tone = _load_service_tone(service, config)
system_parts = [tone] if tone else []
if service.system_prompt:
system_parts.append(service.system_prompt)
system_parts.append(
"با استفاده از اطلاعات کامل مزرعه که در ادامه آمده به سوال کاربر پاسخ بده. "
"اگر داده‌ای در اطلاعات مزرعه وجود دارد، همان را مبنای پاسخ قرار بده و چیزی حدس نزن. "
"اگر داده کافی نبود، این کمبود را شفاف بگو. "
"پاسخ را به زبان کاربر بنویس."
)
system_parts.append(farm_context)
system_parts.append(f"[سوال کاربر]\n{query}")
return "\n\n".join(part for part in system_parts if part)
def _create_audit_log(
farm_uuid: str,
service_id: str,
model: str,
query: str,
system_prompt: str,
messages: list[dict],
) -> "ChatAuditLog":
from .models import ChatAuditLog
log = ChatAuditLog.objects.create(
farm_uuid=farm_uuid,
service_id=service_id,
model=model,
user_query=query,
system_prompt=system_prompt,
messages=messages,
status=ChatAuditLog.STATUS_STARTED,
)
logger.info(
"Created chat audit log id=%s service_id=%s farm_uuid=%s model=%s",
log.id,
service_id,
farm_uuid,
model,
)
return log
def build_rag_context( def build_rag_context(
@@ -76,9 +120,12 @@ def build_rag_context(
service_id: str | None = None, service_id: str | None = None,
) -> str: ) -> str:
""" """
ساخت context برای LLM: دیتای فعلی خاک کاربر + متن‌های مرتبط از RAG. ساخت context برای سرویس‌های توصیه با استفاده از RAG قدیمی.
دیتای کاربر همیشه اول می‌آید تا LLM مقادیر واقعی (مثل pH) را ببیند. این تابع برای سازگاری با irrigation/fertilization حفظ شده است.
""" """
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",
sensor_uuid, sensor_uuid,
@@ -96,16 +143,10 @@ def build_rag_context(
user_soil = build_user_soil_text(sensor_uuid) user_soil = build_user_soil_text(sensor_uuid)
if user_soil and user_soil.strip(): if user_soil and user_soil.strip():
parts.append("[داده‌های فعلی خاک شما]\n" + user_soil.strip()) parts.append("[داده‌های فعلی خاک شما]\n" + user_soil.strip())
logger.debug("Included user soil section sensor_uuid=%s", sensor_uuid)
else:
logger.info("No user soil data found sensor_uuid=%s", sensor_uuid)
weather_text = build_user_weather_text(sensor_uuid) weather_text = build_user_weather_text(sensor_uuid)
if weather_text and weather_text.strip(): if weather_text and weather_text.strip():
parts.append("[پیش‌بینی هواشناسی]\n" + weather_text.strip()) parts.append("[پیش‌بینی هواشناسی]\n" + weather_text.strip())
logger.debug("Included weather section sensor_uuid=%s", sensor_uuid)
else:
logger.info("No weather data found sensor_uuid=%s", sensor_uuid)
results = search_with_query( results = search_with_query(
query, query,
@@ -117,50 +158,35 @@ def build_rag_context(
use_user_embeddings=include_user_embeddings, use_user_embeddings=include_user_embeddings,
) )
if results: if results:
logger.info("Retrieved RAG results count=%s sensor_uuid=%s", len(results), sensor_uuid)
rag_texts = [r.get("text", "").strip() for r in results if r.get("text")] rag_texts = [r.get("text", "").strip() for r in results if r.get("text")]
if rag_texts: if rag_texts:
parts.append("[متن‌های مرجع]\n" + "\n\n---\n\n".join(rag_texts)) parts.append("[متن‌های مرجع]\n" + "\n\n---\n\n".join(rag_texts))
logger.debug("Included RAG reference texts count=%s", len(rag_texts))
else:
logger.info("No RAG results found sensor_uuid=%s kb_name=%s", sensor_uuid, kb_name)
return "\n\n---\n\n".join(parts) if parts else "" return "\n\n---\n\n".join(parts) if parts else ""
def chat_rag_stream( def chat_rag_stream(
query: str, query: str,
sensor_uuid: str | None = None, farm_uuid: str,
config: RAGConfig | None = None, config: RAGConfig | None = None,
limit: int = 5,
system_override: str | None = None, system_override: str | None = None,
kb_name: str | None = None, farm_details: dict | None = None,
service_id: str | None = None,
): ):
logger.info(
"chat_rag_stream started sensor_uuid=%s kb_name=%s limit=%s query_len=%s",
sensor_uuid,
kb_name,
limit,
len(query or ""),
)
""" """
چت RAG با استریم: دیتای embed شده را بازیابی می‌کند و با LLM جواب می‌دهد. چت استریمی با سرویس ثابت `chat` و context مستقیم مزرعه.
فقط دیتای همان کاربر (sensor_uuid) قابل دسترسی است.
Args: Args:
query: پیام کاربر query: پیام کاربر
sensor_uuid: شناسه سنسور کاربر — اجباری farm_uuid: شناسه مزرعه
config: تنظیمات RAG config: تنظیمات RAG
limit: تعداد چانک‌های بازیابی‌شده
system_override: جایگزین system prompt (اختیاری) system_override: جایگزین system prompt (اختیاری)
Yields: Yields:
تک‌تک deltaهای content به‌صورت رشته chunk های استریم پاسخ مدل
""" """
cfg = config or load_rag_config() cfg = config or load_rag_config()
resolved_service_id = service_id or kb_name or _detect_kb_intent(query) service_id = "chat"
service = get_service_config(resolved_service_id, cfg) service = get_service_config(service_id, cfg)
service_llm_config = service.llm service_llm_config = service.llm
service_cfg = RAGConfig( service_cfg = RAGConfig(
embedding=cfg.embedding, embedding=cfg.embedding,
@@ -173,56 +199,97 @@ def chat_rag_stream(
) )
client = get_chat_client(service_cfg) client = get_chat_client(service_cfg)
model = service_llm_config.model model = service_llm_config.model
logger.debug("Loaded service config service_id=%s model=%s", resolved_service_id, model)
detected_kb = kb_name or service.knowledge_base logger.info(
logger.info("Using knowledge base=%s for service_id=%s", detected_kb, resolved_service_id) "chat_rag_stream started service_id=%s farm_uuid=%s query_len=%s",
context = build_rag_context( service_id,
query, farm_uuid,
sensor_uuid, len(query or ""),
config=cfg, )
limit=limit,
kb_name=detected_kb, if farm_details is None:
service_id=resolved_service_id, farm_context = _format_farm_context(farm_uuid)
else:
farm_context = _format_farm_context_from_details(farm_details)
logger.info(
"Loaded farm context for farm_uuid=%s context_len=%s",
farm_uuid,
len(farm_context),
) )
logger.debug("Built context length=%s", len(context))
if system_override is not None: if system_override is not None:
system_content = system_override system_prompt = system_override
else: else:
tone = _load_service_tone(service, cfg) system_prompt = _build_system_prompt(service, query, farm_context, cfg)
if not tone:
tone = _load_tone(cfg)
system_parts = [tone] if tone else []
if service.system_prompt:
system_parts.append(service.system_prompt)
system_parts.append(
"با استفاده از بخش «داده‌های فعلی خاک شما» و «متن‌های مرجع» زیر به سوال کاربر پاسخ بده. "
"برای سوالاتی درباره خاک کاربر (مثل pH، رطوبت، NPK) حتماً از داده‌های فعلی استفاده کن. "
"اطلاعات هواشناسی در بخش «پیش‌بینی هواشناسی» آمده. "
"پاسخ را به زبان کاربر بنویس."
)
if context:
system_parts.append("\n\n" + context)
system_content = "\n".join(system_parts)
messages = [ messages = [
{"role": "system", "content": system_content}, {"role": "system", "content": system_prompt},
{"role": "user", "content": query}, {"role": "user", "content": query},
] ]
logger.info("Prepared messages for model=%s service_id=%s", model, resolved_service_id)
stream = client.chat.completions.create( logger.info(
model=model, "Final prompt prepared service_id=%s farm_uuid=%s model=%s messages_count=%s",
messages=messages, service_id,
stream=True, farm_uuid,
model,
len(messages),
) )
logger.info("Started streaming response from model=%s", model) logger.info("Final system prompt for farm_uuid=%s:\n%s", farm_uuid, system_prompt)
for chunk in stream: audit_log = _create_audit_log(
delta = chunk.choices[0].delta if chunk.choices else None farm_uuid=farm_uuid,
content = delta.content if delta else "" service_id=service_id,
if content: model=model,
logger.debug("Streaming chunk len=%s", len(content)) query=query,
yield content system_prompt=system_prompt,
logger.info("chat_rag_stream completed sensor_uuid=%s", sensor_uuid) messages=messages,
)
response_chunks: list[str] = []
try:
stream = client.chat.completions.create(
model=model,
messages=messages,
stream=True,
)
logger.info(
"Started streaming response id=%s service_id=%s farm_uuid=%s",
audit_log.id,
service_id,
farm_uuid,
)
for chunk in stream:
delta = chunk.choices[0].delta if chunk.choices else None
content = delta.content if delta else ""
if content:
response_chunks.append(content)
yield content
full_response = "".join(response_chunks)
audit_log.response_text = full_response
audit_log.status = ChatAuditLog.STATUS_COMPLETED
audit_log.save(update_fields=["response_text", "status", "updated_at"])
logger.info(
"Completed chat response id=%s farm_uuid=%s response_len=%s response=\n%s",
audit_log.id,
farm_uuid,
len(full_response),
full_response,
)
except Exception as exc:
partial_response = "".join(response_chunks)
audit_log.response_text = partial_response
audit_log.error_message = str(exc)
audit_log.status = ChatAuditLog.STATUS_FAILED
audit_log.save(
update_fields=["response_text", "error_message", "status", "updated_at"]
)
logger.exception(
"Chat request failed id=%s service_id=%s farm_uuid=%s partial_response_len=%s",
audit_log.id,
service_id,
farm_uuid,
len(partial_response),
)
raise
+10
View File
@@ -15,10 +15,15 @@ class EmbeddingConfig:
provider: str provider: str
model: str model: str
batch_size: int = 32 batch_size: int = 32
api_key: str | None = None
api_key_env: str | None = None api_key_env: str | None = None
base_url: str | None = None base_url: str | None = None
avalai_api_key: str | None = None
avalai_base_url: str | None = None avalai_base_url: str | None = None
avalai_api_key_env: str | None = None avalai_api_key_env: str | None = None
arvancloud_api_key: str | None = None
arvancloud_base_url: str | None = None
arvancloud_api_key_env: str | None = None
@dataclass @dataclass
@@ -116,10 +121,15 @@ def load_rag_config(config_path: str | Path | None = None) -> RAGConfig:
provider=emb.get("provider", "sentence_transformers"), provider=emb.get("provider", "sentence_transformers"),
model=emb.get("model", "text-embedding-3-small"), model=emb.get("model", "text-embedding-3-small"),
batch_size=emb.get("batch_size", 32), batch_size=emb.get("batch_size", 32),
api_key=emb.get("api_key"),
api_key_env=emb.get("api_key_env"), api_key_env=emb.get("api_key_env"),
base_url=emb.get("base_url"), base_url=emb.get("base_url"),
avalai_api_key=emb.get("avalai_api_key"),
avalai_base_url=emb.get("avalai_base_url"), avalai_base_url=emb.get("avalai_base_url"),
avalai_api_key_env=emb.get("avalai_api_key_env"), avalai_api_key_env=emb.get("avalai_api_key_env"),
arvancloud_api_key=emb.get("arvancloud_api_key"),
arvancloud_base_url=emb.get("arvancloud_base_url"),
arvancloud_api_key_env=emb.get("arvancloud_api_key_env"),
) )
qd = data.get("qdrant", {}) qd = data.get("qdrant", {})
+3
View File
@@ -3,7 +3,9 @@
""" """
from .api_provider import get_embedding_client from .api_provider import get_embedding_client
from .config import RAGConfig, load_rag_config from .config import RAGConfig, load_rag_config
import logging
logger = logging.getLogger(__name__)
def embed_texts( def embed_texts(
texts: list[str], texts: list[str],
@@ -29,6 +31,7 @@ def embed_texts(
cfg = config or load_rag_config() cfg = config or load_rag_config()
client = get_embedding_client(cfg) client = get_embedding_client(cfg)
model_name = model or cfg.embedding.model model_name = model or cfg.embedding.model
logger.info(model_name)
batch_size = cfg.embedding.batch_size batch_size = cfg.embedding.batch_size
all_embeddings: list[list[float]] = [] all_embeddings: list[list[float]] = []
+33
View File
@@ -0,0 +1,33 @@
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="ChatAuditLog",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("farm_uuid", models.UUIDField(blank=True, help_text="شناسه مزرعه مرتبط با درخواست چت", null=True)),
("service_id", models.CharField(default="chat", help_text="شناسه سرویس RAG استفاده شده برای این درخواست", max_length=64)),
("model", models.CharField(blank=True, help_text="مدل LLM استفاده شده برای پاسخ", max_length=128)),
("user_query", models.TextField(help_text="متن پرسش کاربر")),
("system_prompt", models.TextField(blank=True, help_text="system prompt نهایی ارسال شده به مدل")),
("messages", models.JSONField(blank=True, default=list, help_text="لیست کامل پیام‌های ارسال شده به مدل")),
("response_text", models.TextField(blank=True, help_text="متن کامل پاسخ دریافتی از مدل")),
("error_message", models.TextField(blank=True, help_text="خطای رخ داده هنگام فراخوانی مدل یا استریم")),
("status", models.CharField(choices=[("started", "شروع شده"), ("completed", "تکمیل شده"), ("failed", "ناموفق")], default="started", max_length=16)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"db_table": "rag_chatauditlog",
"ordering": ["-created_at"],
"verbose_name": "لاگ چت RAG",
"verbose_name_plural": "لاگ\u200cهای چت RAG",
},
),
]
+1
View File
@@ -0,0 +1 @@
+62
View File
@@ -0,0 +1,62 @@
from django.db import models
class ChatAuditLog(models.Model):
STATUS_STARTED = "started"
STATUS_COMPLETED = "completed"
STATUS_FAILED = "failed"
STATUS_CHOICES = [
(STATUS_STARTED, "شروع شده"),
(STATUS_COMPLETED, "تکمیل شده"),
(STATUS_FAILED, "ناموفق"),
]
farm_uuid = models.UUIDField(
null=True,
blank=True,
help_text="شناسه مزرعه مرتبط با درخواست چت",
)
service_id = models.CharField(
max_length=64,
default="chat",
help_text="شناسه سرویس RAG استفاده شده برای این درخواست",
)
model = models.CharField(
max_length=128,
blank=True,
help_text="مدل LLM استفاده شده برای پاسخ",
)
user_query = models.TextField(help_text="متن پرسش کاربر")
system_prompt = models.TextField(
blank=True,
help_text="system prompt نهایی ارسال شده به مدل",
)
messages = models.JSONField(
default=list,
blank=True,
help_text="لیست کامل پیام‌های ارسال شده به مدل",
)
response_text = models.TextField(
blank=True,
help_text="متن کامل پاسخ دریافتی از مدل",
)
error_message = models.TextField(
blank=True,
help_text="خطای رخ داده هنگام فراخوانی مدل یا استریم",
)
status = models.CharField(
max_length=16,
choices=STATUS_CHOICES,
default=STATUS_STARTED,
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "rag_chatauditlog"
ordering = ["-created_at"]
verbose_name = "لاگ چت RAG"
verbose_name_plural = "لاگ‌های چت RAG"
def __str__(self):
return f"{self.service_id} - {self.farm_uuid or 'no-farm'} - {self.status}"
+18 -9
View File
@@ -102,7 +102,7 @@ class QdrantVectorStore:
) -> list[dict]: ) -> list[dict]:
""" """
جستجوی شباهت بر اساس query vector. جستجوی شباهت بر اساس query vector.
از query_points استفاده می‌کند (qdrant-client >= 2.0). روی نسخه‌های جدید از query_points و روی نسخه‌های قدیمی‌تر از search استفاده می‌کند.
sensor_uuid: اجباری — فقط chunks مربوط به این سنسور یا __global__ برگردانده می‌شود. sensor_uuid: اجباری — فقط chunks مربوط به این سنسور یا __global__ برگردانده می‌شود.
kb_name: اختیاری — فیلتر بر اساس پایگاه دانش (chat/irrigation/fertilization). kb_name: اختیاری — فیلتر بر اساس پایگاه دانش (chat/irrigation/fertilization).
اگر مشخص شود، فقط chunks همان KB و __all__ برگردانده می‌شود. اگر مشخص شود، فقط chunks همان KB و __all__ برگردانده می‌شود.
@@ -141,14 +141,23 @@ class QdrantVectorStore:
if must_conditions: if must_conditions:
query_filter = qmodels.Filter(must=must_conditions) query_filter = qmodels.Filter(must=must_conditions)
response = self.client.query_points( if hasattr(self.client, "query_points"):
collection_name=self.qdrant.collection_name, response = self.client.query_points(
query=query_vector, collection_name=self.qdrant.collection_name,
limit=limit, query=query_vector,
score_threshold=score_threshold, limit=limit,
query_filter=query_filter, score_threshold=score_threshold,
) query_filter=query_filter,
points = getattr(response, "points", []) or [] )
points = getattr(response, "points", []) or []
else:
points = self.client.search(
collection_name=self.qdrant.collection_name,
query_vector=query_vector,
limit=limit,
score_threshold=score_threshold,
query_filter=query_filter,
)
return [ return [
{ {
+23 -32
View File
@@ -59,8 +59,8 @@ RagValidationErrorResponseSerializer = build_envelope_serializer(
class ChatView(APIView): class ChatView(APIView):
""" """
چت RAG با استریم. چت RAG با استریم.
POST با {"service_id": "...", "query": "متن سوال", "user_id": "شناسه کاربر"} POST با {"query": "متن سوال", "farm_uuid": "شناسه مزرعه"}.
service_id اجباری است. user_id فقط برای سرویس‌هایی که user embeddings دارند اجباری می‌شود. همیشه از سرویس ثابت `chat` استفاده می‌کند و اطلاعات مزرعه را مستقیم به مدل می‌فرستد.
""" """
@extend_schema( @extend_schema(
@@ -70,11 +70,9 @@ class ChatView(APIView):
request=inline_serializer( request=inline_serializer(
name="ChatRequest", name="ChatRequest",
fields={ fields={
"service_id": drf_serializers.CharField(help_text="شناسه سرویس"),
"query": drf_serializers.CharField(required=False, help_text="متن سوال کاربر"), "query": drf_serializers.CharField(required=False, help_text="متن سوال کاربر"),
"message": drf_serializers.CharField(required=False, help_text="نام قبلی فیلد query"), "message": drf_serializers.CharField(required=False, help_text="نام قبلی فیلد query"),
"user_id": drf_serializers.CharField(required=False, help_text="شناسه کاربر"), "farm_uuid": drf_serializers.CharField(help_text="شناسه مزرعه"),
"sensor_uuid": drf_serializers.CharField(required=False, help_text="نام قبلی فیلد user_id"),
}, },
), ),
responses={ responses={
@@ -86,26 +84,29 @@ class ChatView(APIView):
RagChatErrorResponseSerializer, RagChatErrorResponseSerializer,
"پارامترهای ورودی نامعتبر هستند.", "پارامترهای ورودی نامعتبر هستند.",
), ),
404: build_response(
RagChatErrorResponseSerializer,
"مزرعه پیدا نشد.",
),
}, },
examples=[ examples=[
OpenApiExample( OpenApiExample(
"نمونه درخواست", "نمونه درخواست",
value={ value={
"service_id": "support_bot", "farm_uuid": "550e8400-e29b-41d4-a716-446655440000",
"user_id": "12345", "query": "وضعیت مزرعه من چطور است؟",
"query": "How do I reset my password?",
}, },
request_only=True, request_only=True,
), ),
], ],
) )
def post(self, request: Request): def post(self, request: Request):
from .config import load_rag_config, get_service_config from farm_data.services import get_farm_details
from .config import load_rag_config
data = request.data if request.method == "POST" else request.query_params data = request.data if request.method == "POST" else request.query_params
service_id = data.get("service_id")
message = data.get("query", data.get("message")) message = data.get("query", data.get("message"))
user_id = data.get("user_id", data.get("sensor_uuid")) farm_uuid = data.get("farm_uuid")
if not message or not isinstance(message, str): if not message or not isinstance(message, str):
return Response( return Response(
{"code": 400, "msg": "پارامتر query الزامی است."}, {"code": 400, "msg": "پارامتر query الزامی است."},
@@ -117,42 +118,32 @@ class ChatView(APIView):
{"code": 400, "msg": "پیام نباید خالی باشد."}, {"code": 400, "msg": "پیام نباید خالی باشد."},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
if not service_id or not isinstance(service_id, str): if not farm_uuid or not isinstance(farm_uuid, str):
return Response( return Response(
{"code": 400, "msg": "پارامتر service_id الزامی است."}, {"code": 400, "msg": "پارامتر farm_uuid الزامی است."},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
service_id = str(service_id).strip() farm_uuid = str(farm_uuid).strip()
if not service_id: if not farm_uuid:
return Response( return Response(
{"code": 400, "msg": "service_id نباید خالی باشد."}, {"code": 400, "msg": "farm_uuid نباید خالی باشد."},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
cfg = load_rag_config() cfg = load_rag_config()
try: farm_details = get_farm_details(farm_uuid)
service = get_service_config(service_id, cfg) if farm_details is None:
except KeyError:
return Response( return Response(
{"code": 400, "msg": f"service_id نامعتبر است: {service_id}"}, {"code": 404, "msg": "farm پیدا نشد."},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_404_NOT_FOUND,
)
if user_id is not None:
user_id = str(user_id).strip()
if not user_id:
user_id = None
if service.use_user_embeddings and not user_id:
return Response(
{"code": 400, "msg": "برای این service_id، پارامتر user_id الزامی است."},
status=status.HTTP_400_BAD_REQUEST,
) )
def generate(): def generate():
try: try:
for chunk in chat_rag_stream( for chunk in chat_rag_stream(
message, message,
sensor_uuid=user_id, farm_uuid=farm_uuid,
service_id=service_id,
config=cfg, config=cfg,
farm_details=farm_details,
): ):
yield chunk yield chunk
except Exception as e: except Exception as e: