UPDATE
This commit is contained in:
@@ -1,21 +1,25 @@
|
||||
# تنظیمات RAG برای پایگاه دانش CropLogic
|
||||
|
||||
embedding:
|
||||
provider: "gapgpt" # gapgpt یا avalai
|
||||
model: "text-embedding-3-small"
|
||||
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: 1536 # متناسب با text-embedding-3-small
|
||||
vector_size: 1024 # متناسب با BGE-M3
|
||||
|
||||
chunking:
|
||||
max_chunk_tokens: 500
|
||||
|
||||
@@ -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
@@ -24,7 +24,7 @@ services:
|
||||
PMA_PORT: 3306
|
||||
UPLOAD_LIMIT: 64M
|
||||
ports:
|
||||
- "8082:80"
|
||||
- "8083:80"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
2026-04-07 07:48:04,983 [INFO] django.utils.autoreload: Watching for file changes with StatReloader
|
||||
@@ -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
@@ -1,30 +1,5 @@
|
||||
"""
|
||||
ماژول RAG — پایگاه دانش CropLogic
|
||||
فاز یک: Qdrant بهعنوان vector store
|
||||
ماژول RAG — برای جلوگیری از AppRegistryNotReady این فایل import سنگین انجام نمیدهد.
|
||||
"""
|
||||
|
||||
from .chat import chat_rag_stream
|
||||
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",
|
||||
]
|
||||
__all__: list[str] = []
|
||||
|
||||
+21
-8
@@ -1,7 +1,5 @@
|
||||
"""
|
||||
Adapter Pattern برای API providers — سوئیچ بین GapGPT و Avalai
|
||||
تنظیمات فعلی: GapGPT بهعنوان provider اصلی
|
||||
Avalai بهعنوان fallback نگه داشته شده.
|
||||
Adapter Pattern برای API providers — سوئیچ بین GapGPT، Avalai و ArvanCloud AI.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
@@ -20,22 +18,37 @@ def _mask_secret(value: str | None) -> str:
|
||||
return "****"
|
||||
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:
|
||||
"""
|
||||
ساخت کلاینت OpenAI برای Embedding بر اساس provider فعال.
|
||||
provider از config.embedding.provider خوانده میشود: "gapgpt" یا "avalai"
|
||||
provider از config.embedding.provider خوانده میشود.
|
||||
"""
|
||||
cfg = config or load_rag_config()
|
||||
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"
|
||||
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"
|
||||
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:
|
||||
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"
|
||||
logger.info(
|
||||
"embedding base_url=%s api_key=%s",
|
||||
|
||||
+173
-106
@@ -1,13 +1,12 @@
|
||||
"""
|
||||
چت RAG با استریم — استفاده از دیتای embed شده کاربر و Adapter API (GapGPT / Avalai)
|
||||
چت RAG برای API چت عمومی — استفاده مستقیم از داده مزرعه بدون retrieval/embedding.
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from .config import load_rag_config, RAGConfig, get_service_config, ServiceConfig
|
||||
from .api_provider import get_chat_client
|
||||
from .retrieve import search_with_query
|
||||
from .user_data import build_user_soil_text, build_user_weather_text
|
||||
from .config import RAGConfig, ServiceConfig, get_service_config, load_rag_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -19,30 +18,12 @@ def _load_tone(config: RAGConfig | None) -> str:
|
||||
chat_kb = cfg.knowledge_bases.get("chat")
|
||||
if chat_kb:
|
||||
tone_path = base / chat_kb.tone_file
|
||||
logger.debug("Loading default tone from path=%s", tone_path)
|
||||
if tone_path.exists():
|
||||
logger.debug("Default tone file found: %s", tone_path)
|
||||
return tone_path.read_text(encoding="utf-8").strip()
|
||||
logger.warning("Default tone file not found: %s", tone_path)
|
||||
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:
|
||||
cfg = config or load_rag_config()
|
||||
if service.tone_file:
|
||||
@@ -50,21 +31,84 @@ def _load_service_tone(service: ServiceConfig, config: RAGConfig | None = None)
|
||||
tone_path = base / service.tone_file
|
||||
if tone_path.exists():
|
||||
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:
|
||||
"""تشخیص ساده نوع پایگاه دانش مورد نیاز از روی متن سوال."""
|
||||
q = query.lower()
|
||||
irrigation_keywords = {"آبیاری", "آب", "رطوبت", "irrigation", "water", "et0", "بارش", "خشکی"}
|
||||
fertilization_keywords = {"کود", "کودهی", "fertiliz", "npk", "ازت", "فسفر", "پتاسیم", "nitrogen", "phosphorus", "potassium"}
|
||||
if any(kw in q for kw in irrigation_keywords):
|
||||
return "irrigation"
|
||||
if any(kw in q for kw in fertilization_keywords):
|
||||
logger.info("Detected KB intent=fertilization")
|
||||
return "fertilization"
|
||||
logger.info("Detected KB intent=chat")
|
||||
return "chat"
|
||||
def _format_farm_context(farm_uuid: str) -> str:
|
||||
from farm_data.services import get_farm_details
|
||||
|
||||
farm_details = get_farm_details(farm_uuid)
|
||||
if not farm_details:
|
||||
raise ValueError("farm_uuid نامعتبر است یا اطلاعات مزرعه پیدا نشد.")
|
||||
|
||||
serialized = json.dumps(
|
||||
farm_details,
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
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(
|
||||
@@ -76,9 +120,12 @@ def build_rag_context(
|
||||
service_id: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
ساخت context برای LLM: دیتای فعلی خاک کاربر + متنهای مرتبط از RAG.
|
||||
دیتای کاربر همیشه اول میآید تا LLM مقادیر واقعی (مثل pH) را ببیند.
|
||||
ساخت context برای سرویسهای توصیه با استفاده از RAG قدیمی.
|
||||
این تابع برای سازگاری با irrigation/fertilization حفظ شده است.
|
||||
"""
|
||||
from .retrieve import search_with_query
|
||||
from .user_data import build_user_soil_text, build_user_weather_text
|
||||
|
||||
logger.info(
|
||||
"Building RAG context sensor_uuid=%s kb_name=%s limit=%s query_len=%s",
|
||||
sensor_uuid,
|
||||
@@ -96,16 +143,10 @@ def build_rag_context(
|
||||
user_soil = build_user_soil_text(sensor_uuid)
|
||||
if user_soil and 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)
|
||||
if weather_text and 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(
|
||||
query,
|
||||
@@ -117,50 +158,35 @@ def build_rag_context(
|
||||
use_user_embeddings=include_user_embeddings,
|
||||
)
|
||||
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")]
|
||||
if 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 ""
|
||||
|
||||
|
||||
def chat_rag_stream(
|
||||
query: str,
|
||||
sensor_uuid: str | None = None,
|
||||
farm_uuid: str,
|
||||
config: RAGConfig | None = None,
|
||||
limit: int = 5,
|
||||
system_override: str | None = None,
|
||||
kb_name: str | None = None,
|
||||
service_id: str | None = None,
|
||||
farm_details: dict | 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 جواب میدهد.
|
||||
فقط دیتای همان کاربر (sensor_uuid) قابل دسترسی است.
|
||||
چت استریمی با سرویس ثابت `chat` و context مستقیم مزرعه.
|
||||
|
||||
Args:
|
||||
query: پیام کاربر
|
||||
sensor_uuid: شناسه سنسور کاربر — اجباری
|
||||
farm_uuid: شناسه مزرعه
|
||||
config: تنظیمات RAG
|
||||
limit: تعداد چانکهای بازیابیشده
|
||||
system_override: جایگزین system prompt (اختیاری)
|
||||
|
||||
Yields:
|
||||
تکتک deltaهای content بهصورت رشته
|
||||
chunk های استریم پاسخ مدل
|
||||
"""
|
||||
cfg = config or load_rag_config()
|
||||
resolved_service_id = service_id or kb_name or _detect_kb_intent(query)
|
||||
service = get_service_config(resolved_service_id, cfg)
|
||||
service_id = "chat"
|
||||
service = get_service_config(service_id, cfg)
|
||||
service_llm_config = service.llm
|
||||
service_cfg = RAGConfig(
|
||||
embedding=cfg.embedding,
|
||||
@@ -173,56 +199,97 @@ def chat_rag_stream(
|
||||
)
|
||||
client = get_chat_client(service_cfg)
|
||||
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("Using knowledge base=%s for service_id=%s", detected_kb, resolved_service_id)
|
||||
context = build_rag_context(
|
||||
query,
|
||||
sensor_uuid,
|
||||
config=cfg,
|
||||
limit=limit,
|
||||
kb_name=detected_kb,
|
||||
service_id=resolved_service_id,
|
||||
logger.info(
|
||||
"chat_rag_stream started service_id=%s farm_uuid=%s query_len=%s",
|
||||
service_id,
|
||||
farm_uuid,
|
||||
len(query or ""),
|
||||
)
|
||||
|
||||
if farm_details is None:
|
||||
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:
|
||||
system_content = system_override
|
||||
system_prompt = system_override
|
||||
else:
|
||||
tone = _load_service_tone(service, 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)
|
||||
system_prompt = _build_system_prompt(service, query, farm_context, cfg)
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": system_content},
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": query},
|
||||
]
|
||||
logger.info("Prepared messages for model=%s service_id=%s", model, resolved_service_id)
|
||||
|
||||
stream = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=messages,
|
||||
stream=True,
|
||||
logger.info(
|
||||
"Final prompt prepared service_id=%s farm_uuid=%s model=%s messages_count=%s",
|
||||
service_id,
|
||||
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:
|
||||
delta = chunk.choices[0].delta if chunk.choices else None
|
||||
content = delta.content if delta else ""
|
||||
if content:
|
||||
logger.debug("Streaming chunk len=%s", len(content))
|
||||
yield content
|
||||
logger.info("chat_rag_stream completed sensor_uuid=%s", sensor_uuid)
|
||||
audit_log = _create_audit_log(
|
||||
farm_uuid=farm_uuid,
|
||||
service_id=service_id,
|
||||
model=model,
|
||||
query=query,
|
||||
system_prompt=system_prompt,
|
||||
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
|
||||
|
||||
@@ -15,10 +15,15 @@ class EmbeddingConfig:
|
||||
provider: str
|
||||
model: str
|
||||
batch_size: int = 32
|
||||
api_key: str | None = None
|
||||
api_key_env: str | None = None
|
||||
base_url: str | None = None
|
||||
avalai_api_key: str | None = None
|
||||
avalai_base_url: 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
|
||||
@@ -116,10 +121,15 @@ def load_rag_config(config_path: str | Path | None = None) -> RAGConfig:
|
||||
provider=emb.get("provider", "sentence_transformers"),
|
||||
model=emb.get("model", "text-embedding-3-small"),
|
||||
batch_size=emb.get("batch_size", 32),
|
||||
api_key=emb.get("api_key"),
|
||||
api_key_env=emb.get("api_key_env"),
|
||||
base_url=emb.get("base_url"),
|
||||
avalai_api_key=emb.get("avalai_api_key"),
|
||||
avalai_base_url=emb.get("avalai_base_url"),
|
||||
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", {})
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
"""
|
||||
from .api_provider import get_embedding_client
|
||||
from .config import RAGConfig, load_rag_config
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def embed_texts(
|
||||
texts: list[str],
|
||||
@@ -29,6 +31,7 @@ def embed_texts(
|
||||
cfg = config or load_rag_config()
|
||||
client = get_embedding_client(cfg)
|
||||
model_name = model or cfg.embedding.model
|
||||
logger.info(model_name)
|
||||
batch_size = cfg.embedding.batch_size
|
||||
|
||||
all_embeddings: list[list[float]] = []
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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
@@ -102,7 +102,7 @@ class QdrantVectorStore:
|
||||
) -> list[dict]:
|
||||
"""
|
||||
جستجوی شباهت بر اساس query vector.
|
||||
از query_points استفاده میکند (qdrant-client >= 2.0).
|
||||
روی نسخههای جدید از query_points و روی نسخههای قدیمیتر از search استفاده میکند.
|
||||
sensor_uuid: اجباری — فقط chunks مربوط به این سنسور یا __global__ برگردانده میشود.
|
||||
kb_name: اختیاری — فیلتر بر اساس پایگاه دانش (chat/irrigation/fertilization).
|
||||
اگر مشخص شود، فقط chunks همان KB و __all__ برگردانده میشود.
|
||||
@@ -141,14 +141,23 @@ class QdrantVectorStore:
|
||||
if must_conditions:
|
||||
query_filter = qmodels.Filter(must=must_conditions)
|
||||
|
||||
response = self.client.query_points(
|
||||
collection_name=self.qdrant.collection_name,
|
||||
query=query_vector,
|
||||
limit=limit,
|
||||
score_threshold=score_threshold,
|
||||
query_filter=query_filter,
|
||||
)
|
||||
points = getattr(response, "points", []) or []
|
||||
if hasattr(self.client, "query_points"):
|
||||
response = self.client.query_points(
|
||||
collection_name=self.qdrant.collection_name,
|
||||
query=query_vector,
|
||||
limit=limit,
|
||||
score_threshold=score_threshold,
|
||||
query_filter=query_filter,
|
||||
)
|
||||
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 [
|
||||
{
|
||||
|
||||
+23
-32
@@ -59,8 +59,8 @@ RagValidationErrorResponseSerializer = build_envelope_serializer(
|
||||
class ChatView(APIView):
|
||||
"""
|
||||
چت RAG با استریم.
|
||||
POST با {"service_id": "...", "query": "متن سوال", "user_id": "شناسه کاربر"}
|
||||
service_id اجباری است. user_id فقط برای سرویسهایی که user embeddings دارند اجباری میشود.
|
||||
POST با {"query": "متن سوال", "farm_uuid": "شناسه مزرعه"}.
|
||||
همیشه از سرویس ثابت `chat` استفاده میکند و اطلاعات مزرعه را مستقیم به مدل میفرستد.
|
||||
"""
|
||||
|
||||
@extend_schema(
|
||||
@@ -70,11 +70,9 @@ class ChatView(APIView):
|
||||
request=inline_serializer(
|
||||
name="ChatRequest",
|
||||
fields={
|
||||
"service_id": drf_serializers.CharField(help_text="شناسه سرویس"),
|
||||
"query": drf_serializers.CharField(required=False, help_text="متن سوال کاربر"),
|
||||
"message": drf_serializers.CharField(required=False, help_text="نام قبلی فیلد query"),
|
||||
"user_id": drf_serializers.CharField(required=False, help_text="شناسه کاربر"),
|
||||
"sensor_uuid": drf_serializers.CharField(required=False, help_text="نام قبلی فیلد user_id"),
|
||||
"farm_uuid": drf_serializers.CharField(help_text="شناسه مزرعه"),
|
||||
},
|
||||
),
|
||||
responses={
|
||||
@@ -86,26 +84,29 @@ class ChatView(APIView):
|
||||
RagChatErrorResponseSerializer,
|
||||
"پارامترهای ورودی نامعتبر هستند.",
|
||||
),
|
||||
404: build_response(
|
||||
RagChatErrorResponseSerializer,
|
||||
"مزرعه پیدا نشد.",
|
||||
),
|
||||
},
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"نمونه درخواست",
|
||||
value={
|
||||
"service_id": "support_bot",
|
||||
"user_id": "12345",
|
||||
"query": "How do I reset my password?",
|
||||
"farm_uuid": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"query": "وضعیت مزرعه من چطور است؟",
|
||||
},
|
||||
request_only=True,
|
||||
),
|
||||
],
|
||||
)
|
||||
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
|
||||
service_id = data.get("service_id")
|
||||
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):
|
||||
return Response(
|
||||
{"code": 400, "msg": "پارامتر query الزامی است."},
|
||||
@@ -117,42 +118,32 @@ class ChatView(APIView):
|
||||
{"code": 400, "msg": "پیام نباید خالی باشد."},
|
||||
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(
|
||||
{"code": 400, "msg": "پارامتر service_id الزامی است."},
|
||||
{"code": 400, "msg": "پارامتر farm_uuid الزامی است."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
service_id = str(service_id).strip()
|
||||
if not service_id:
|
||||
farm_uuid = str(farm_uuid).strip()
|
||||
if not farm_uuid:
|
||||
return Response(
|
||||
{"code": 400, "msg": "service_id نباید خالی باشد."},
|
||||
{"code": 400, "msg": "farm_uuid نباید خالی باشد."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
cfg = load_rag_config()
|
||||
try:
|
||||
service = get_service_config(service_id, cfg)
|
||||
except KeyError:
|
||||
farm_details = get_farm_details(farm_uuid)
|
||||
if farm_details is None:
|
||||
return Response(
|
||||
{"code": 400, "msg": f"service_id نامعتبر است: {service_id}"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
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,
|
||||
{"code": 404, "msg": "farm پیدا نشد."},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
def generate():
|
||||
try:
|
||||
for chunk in chat_rag_stream(
|
||||
message,
|
||||
sensor_uuid=user_id,
|
||||
service_id=service_id,
|
||||
farm_uuid=farm_uuid,
|
||||
config=cfg,
|
||||
farm_details=farm_details,
|
||||
):
|
||||
yield chunk
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user