UPDATE
This commit is contained in:
+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