Refactor user data handling and enhance chat functionality
- Removed deprecated user_info files and paths from configuration. - Added user soil data integration in chat context to improve response accuracy. - Updated build_rag_context and chat_rag_stream functions to include sensor_uuid for user-specific data retrieval. - Enhanced load_sources function to load user data from the database. - Implemented filtering in search_with_query and QdrantVectorStore to isolate user data based on sensor_uuid. - Introduced Celery Beat schedule for periodic user data ingestion.
This commit is contained in:
@@ -26,4 +26,3 @@ llm:
|
|||||||
|
|
||||||
tone_file: "config/tone.txt"
|
tone_file: "config/tone.txt"
|
||||||
knowledge_base_path: "config/knowledge_base"
|
knowledge_base_path: "config/knowledge_base"
|
||||||
user_info_path: "config/user_info"
|
|
||||||
|
|||||||
@@ -107,3 +107,11 @@ CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "redis://localhost:6379/
|
|||||||
CELERY_RESULT_BACKEND = os.environ.get("CELERY_RESULT_BACKEND", "redis://localhost:6379/0")
|
CELERY_RESULT_BACKEND = os.environ.get("CELERY_RESULT_BACKEND", "redis://localhost:6379/0")
|
||||||
CELERY_ACCEPT_CONTENT = ["json"]
|
CELERY_ACCEPT_CONTENT = ["json"]
|
||||||
CELERY_TASK_SERIALIZER = "json"
|
CELERY_TASK_SERIALIZER = "json"
|
||||||
|
|
||||||
|
# Celery Beat — embed دیتای کاربران هر ۶ ساعت
|
||||||
|
CELERY_BEAT_SCHEDULE = {
|
||||||
|
"rag-ingest-periodic": {
|
||||||
|
"task": "rag.tasks.rag_ingest_task",
|
||||||
|
"schedule": 6 * 60 * 60, # ۶ ساعت
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# اطلاعات کاربران
|
|
||||||
|
|
||||||
فایلهای `.txt` و `.md` این پوشه بهعنوان اطلاعات هر کاربر embed و ذخیره میشوند.
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
{
|
|
||||||
"farm": {
|
|
||||||
"name": "مزرعه نمونه گلستان",
|
|
||||||
"location": {
|
|
||||||
"latitude": 36.2,
|
|
||||||
"longitude": 52.5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"soil_data": {
|
|
||||||
"0-5cm": {
|
|
||||||
"phh2o": 7.2,
|
|
||||||
"clay": 25,
|
|
||||||
"sand": 45,
|
|
||||||
"silt": 30,
|
|
||||||
"soc": 1.4,
|
|
||||||
"nitrogen": 0.12
|
|
||||||
},
|
|
||||||
"5-15cm": {
|
|
||||||
"phh2o": 7.4,
|
|
||||||
"clay": 28,
|
|
||||||
"sand": 42,
|
|
||||||
"silt": 30,
|
|
||||||
"soc": 1.1,
|
|
||||||
"nitrogen": 0.09
|
|
||||||
},
|
|
||||||
"15-30cm": {
|
|
||||||
"phh2o": 7.5,
|
|
||||||
"clay": 30,
|
|
||||||
"sand": 40,
|
|
||||||
"silt": 30,
|
|
||||||
"soc": 0.8,
|
|
||||||
"nitrogen": 0.07
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sensor_readings": {
|
|
||||||
"soil_moisture": 32,
|
|
||||||
"soil_temperature": 24.5,
|
|
||||||
"soil_ph": 7.1,
|
|
||||||
"electrical_conductivity": 2.1,
|
|
||||||
"nitrogen": 15,
|
|
||||||
"phosphorus": 8,
|
|
||||||
"potassium": 180
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,7 @@ from .config import load_rag_config
|
|||||||
from .embedding import embed_single, embed_texts
|
from .embedding import embed_single, embed_texts
|
||||||
from .ingest import ingest, load_sources
|
from .ingest import ingest, load_sources
|
||||||
from .retrieve import search_with_query
|
from .retrieve import search_with_query
|
||||||
|
from .user_data import build_user_soil_text, load_user_sources
|
||||||
from .vector_store import QdrantVectorStore
|
from .vector_store import QdrantVectorStore
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -22,6 +23,8 @@ __all__ = [
|
|||||||
"ingest",
|
"ingest",
|
||||||
"load_rag_config",
|
"load_rag_config",
|
||||||
"load_sources",
|
"load_sources",
|
||||||
|
"load_user_sources",
|
||||||
|
"build_user_soil_text",
|
||||||
"QdrantVectorStore",
|
"QdrantVectorStore",
|
||||||
"search_with_query",
|
"search_with_query",
|
||||||
]
|
]
|
||||||
|
|||||||
+34
-15
@@ -8,6 +8,7 @@ from openai import OpenAI
|
|||||||
|
|
||||||
from .config import load_rag_config, RAGConfig
|
from .config import load_rag_config, RAGConfig
|
||||||
from .retrieve import search_with_query
|
from .retrieve import search_with_query
|
||||||
|
from .user_data import build_user_soil_text
|
||||||
|
|
||||||
|
|
||||||
def _get_chat_client(config: RAGConfig | None) -> OpenAI:
|
def _get_chat_client(config: RAGConfig | None) -> OpenAI:
|
||||||
@@ -32,32 +33,49 @@ def _load_tone(config: RAGConfig | None) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def build_rag_context(query: str, config: RAGConfig | None = None, limit: int = 5) -> str:
|
def build_rag_context(
|
||||||
|
query: str,
|
||||||
|
sensor_uuid: str,
|
||||||
|
config: RAGConfig | None = None,
|
||||||
|
limit: int = 8,
|
||||||
|
) -> str:
|
||||||
"""
|
"""
|
||||||
بازیابی متنهای مرتبط از RAG برای کوئری کاربر.
|
ساخت context برای LLM: دیتای فعلی خاک کاربر + متنهای مرتبط از RAG.
|
||||||
|
دیتای کاربر همیشه اول میآید تا LLM مقادیر واقعی (مثل pH) را ببیند.
|
||||||
"""
|
"""
|
||||||
results = search_with_query(query, limit=limit, config=config)
|
parts: list[str] = []
|
||||||
if not results:
|
|
||||||
return ""
|
# ۱. دیتای فعلی خاک کاربر از DB — همیشه اول (برای سوالاتی مثل «pH خاک من چند است»)
|
||||||
parts = []
|
user_soil = build_user_soil_text(sensor_uuid)
|
||||||
for r in results:
|
if user_soil and user_soil.strip():
|
||||||
text = r.get("text", "").strip()
|
parts.append("[دادههای فعلی خاک شما]\n" + user_soil.strip())
|
||||||
if text:
|
|
||||||
parts.append(text)
|
# ۲. متنهای مرتبط از RAG
|
||||||
return "\n\n---\n\n".join(parts)
|
results = search_with_query(
|
||||||
|
query, sensor_uuid=sensor_uuid, limit=limit, config=config
|
||||||
|
)
|
||||||
|
if results:
|
||||||
|
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))
|
||||||
|
|
||||||
|
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,
|
||||||
config: RAGConfig | None = None,
|
config: RAGConfig | None = None,
|
||||||
limit: int = 5,
|
limit: int = 5,
|
||||||
system_override: str | None = None,
|
system_override: str | None = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
چت RAG با استریم: دیتای embed شده را بازیابی میکند و با LLM جواب میدهد.
|
چت RAG با استریم: دیتای embed شده را بازیابی میکند و با LLM جواب میدهد.
|
||||||
|
فقط دیتای همان کاربر (sensor_uuid) قابل دسترسی است.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query: پیام کاربر
|
query: پیام کاربر
|
||||||
|
sensor_uuid: شناسه سنسور کاربر — اجباری
|
||||||
config: تنظیمات RAG
|
config: تنظیمات RAG
|
||||||
limit: تعداد چانکهای بازیابیشده
|
limit: تعداد چانکهای بازیابیشده
|
||||||
system_override: جایگزین system prompt (اختیاری)
|
system_override: جایگزین system prompt (اختیاری)
|
||||||
@@ -69,7 +87,7 @@ def chat_rag_stream(
|
|||||||
client = _get_chat_client(cfg)
|
client = _get_chat_client(cfg)
|
||||||
model = cfg.llm.model
|
model = cfg.llm.model
|
||||||
|
|
||||||
context = build_rag_context(query, config=cfg, limit=limit)
|
context = build_rag_context(query, sensor_uuid, config=cfg, limit=limit)
|
||||||
|
|
||||||
if system_override is not None:
|
if system_override is not None:
|
||||||
system_content = system_override
|
system_content = system_override
|
||||||
@@ -77,11 +95,12 @@ def chat_rag_stream(
|
|||||||
tone = _load_tone(cfg)
|
tone = _load_tone(cfg)
|
||||||
system_parts = [tone] if tone else []
|
system_parts = [tone] if tone else []
|
||||||
system_parts.append(
|
system_parts.append(
|
||||||
"با استفاده از بخش «متنهای مرجع» زیر به سوال کاربر پاسخ بده. "
|
"با استفاده از بخش «دادههای فعلی خاک شما» و «متنهای مرجع» زیر به سوال کاربر پاسخ بده. "
|
||||||
"فقط در حد نیاز از مرجع استفاده کن و پاسخ را به زبان کاربر بنویس."
|
"برای سوالاتی درباره خاک کاربر (مثل pH، رطوبت، NPK) حتماً از دادههای فعلی استفاده کن. "
|
||||||
|
"پاسخ را به زبان کاربر بنویس."
|
||||||
)
|
)
|
||||||
if context:
|
if context:
|
||||||
system_parts.append("\n\nمتنهای مرجع:\n" + context)
|
system_parts.append("\n\n" + context)
|
||||||
system_content = "\n".join(system_parts)
|
system_content = "\n".join(system_parts)
|
||||||
|
|
||||||
messages = [
|
messages = [
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ class RAGConfig:
|
|||||||
llm: LLMConfig = field(default_factory=LLMConfig)
|
llm: LLMConfig = field(default_factory=LLMConfig)
|
||||||
tone_file: str = "config/tone.txt"
|
tone_file: str = "config/tone.txt"
|
||||||
knowledge_base_path: str = "config/knowledge_base"
|
knowledge_base_path: str = "config/knowledge_base"
|
||||||
user_info_path: str = "config/user_info"
|
|
||||||
chromadb: dict[str, Any] = field(default_factory=dict)
|
chromadb: dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
@@ -104,6 +103,5 @@ def load_rag_config(config_path: str | Path | None = None) -> RAGConfig:
|
|||||||
llm=llm,
|
llm=llm,
|
||||||
tone_file=data.get("tone_file", "config/tone.txt"),
|
tone_file=data.get("tone_file", "config/tone.txt"),
|
||||||
knowledge_base_path=data.get("knowledge_base_path", "config/knowledge_base"),
|
knowledge_base_path=data.get("knowledge_base_path", "config/knowledge_base"),
|
||||||
user_info_path=data.get("user_info_path", "config/user_info"),
|
|
||||||
chromadb=data.get("chromadb", {}),
|
chromadb=data.get("chromadb", {}),
|
||||||
)
|
)
|
||||||
|
|||||||
+25
-21
@@ -2,9 +2,9 @@
|
|||||||
پایپلاین ورودی RAG: خواندن، چانک، embed و ذخیره در vector store
|
پایپلاین ورودی RAG: خواندن، چانک، embed و ذخیره در vector store
|
||||||
|
|
||||||
سه منبع:
|
سه منبع:
|
||||||
۱. لحن (tone)
|
۱. لحن (tone) — sensor_uuid=__global__
|
||||||
۲. پایگاه دانش (knowledge base)
|
۲. پایگاه دانش (knowledge base) — sensor_uuid=__global__
|
||||||
۳. اطلاعات هر کاربر (user info)
|
۳. دیتای خاک هر کاربر از DB (sensor_data + soil_data) — sensor_uuid=uuid
|
||||||
"""
|
"""
|
||||||
import uuid
|
import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -12,11 +12,14 @@ from pathlib import Path
|
|||||||
from .chunker import chunk_text, chunk_texts
|
from .chunker import chunk_text, chunk_texts
|
||||||
from .config import load_rag_config, RAGConfig
|
from .config import load_rag_config, RAGConfig
|
||||||
from .embedding import embed_texts
|
from .embedding import embed_texts
|
||||||
|
from .user_data import load_user_sources
|
||||||
from .vector_store import QdrantVectorStore
|
from .vector_store import QdrantVectorStore
|
||||||
|
|
||||||
# پسوندهای قابل خواندن
|
# پسوندهای قابل خواندن
|
||||||
TEXT_EXTENSIONS = {".txt", ".md", ".rst", ".json"}
|
TEXT_EXTENSIONS = {".txt", ".md", ".rst", ".json"}
|
||||||
|
|
||||||
|
SENSOR_UUID_GLOBAL = "__global__"
|
||||||
|
|
||||||
|
|
||||||
def _resolve_path(base: Path, p: str) -> Path:
|
def _resolve_path(base: Path, p: str) -> Path:
|
||||||
"""تبدیل مسیر نسبی به مطلق نسبت به base پروژه."""
|
"""تبدیل مسیر نسبی به مطلق نسبت به base پروژه."""
|
||||||
@@ -54,41 +57,37 @@ def _load_files_from_dir(dir_path: Path, prefix: str = "kb") -> list[tuple[str,
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def load_sources(config: RAGConfig | None = None) -> list[tuple[str, str]]:
|
def load_sources(config: RAGConfig | None = None) -> list[tuple[str, str, str]]:
|
||||||
"""
|
"""
|
||||||
بارگذاری سه منبع: لحن، پایگاه دانش، اطلاعات کاربر.
|
بارگذاری سه منبع: لحن، پایگاه دانش، دیتای کاربر از DB.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
[(source_id, content), ...]
|
[(source_id, content, sensor_uuid), ...]
|
||||||
source_id مثال: tone, kb:file.txt, user:profile.txt
|
sensor_uuid: __global__ برای tone/kb، uuid سنسور برای user
|
||||||
"""
|
"""
|
||||||
cfg = config or load_rag_config()
|
cfg = config or load_rag_config()
|
||||||
base = Path(__file__).resolve().parent.parent
|
base = Path(__file__).resolve().parent.parent
|
||||||
sources: list[tuple[str, str]] = []
|
sources: list[tuple[str, str, str]] = []
|
||||||
|
|
||||||
# ۱. لحن
|
# ۱. لحن
|
||||||
tone_path = _resolve_path(base, cfg.tone_file)
|
tone_path = _resolve_path(base, cfg.tone_file)
|
||||||
content = _load_file(tone_path)
|
content = _load_file(tone_path)
|
||||||
if content:
|
if content:
|
||||||
sources.append(("tone", content))
|
sources.append(("tone", content, SENSOR_UUID_GLOBAL))
|
||||||
|
|
||||||
# ۲. پایگاه دانش
|
# ۲. پایگاه دانش
|
||||||
kb_path = _resolve_path(base, cfg.knowledge_base_path)
|
kb_path = _resolve_path(base, cfg.knowledge_base_path)
|
||||||
for sid, c in _load_files_from_dir(kb_path, prefix="kb"):
|
for sid, c in _load_files_from_dir(kb_path, prefix="kb"):
|
||||||
sources.append((sid, c))
|
sources.append((sid, c, SENSOR_UUID_GLOBAL))
|
||||||
if kb_path.is_file():
|
if kb_path.is_file():
|
||||||
content = _load_file(kb_path)
|
content = _load_file(kb_path)
|
||||||
if content:
|
if content:
|
||||||
sources.append((f"kb:{kb_path.name}", content))
|
sources.append((f"kb:{kb_path.name}", content, SENSOR_UUID_GLOBAL))
|
||||||
|
|
||||||
# ۳. اطلاعات کاربر
|
# ۳. دیتای کاربران از sensor_data + soil_data
|
||||||
user_path = _resolve_path(base, cfg.user_info_path)
|
for sid, content in load_user_sources():
|
||||||
for sid, c in _load_files_from_dir(user_path, prefix="user"):
|
sensor_uuid = sid.replace("user:", "")
|
||||||
sources.append((sid, c))
|
sources.append((sid, content, sensor_uuid))
|
||||||
if user_path.is_file():
|
|
||||||
content = _load_file(user_path)
|
|
||||||
if content:
|
|
||||||
sources.append((f"user:{user_path.name}", content))
|
|
||||||
|
|
||||||
return sources
|
return sources
|
||||||
|
|
||||||
@@ -96,6 +95,7 @@ def load_sources(config: RAGConfig | None = None) -> list[tuple[str, str]]:
|
|||||||
def ingest(recreate: bool = False, config: RAGConfig | None = None) -> dict:
|
def ingest(recreate: bool = False, config: RAGConfig | None = None) -> dict:
|
||||||
"""
|
"""
|
||||||
ورودی کامل: منابع را میخواند، چانک میکند، embed میکند و به vector store میفرستد.
|
ورودی کامل: منابع را میخواند، چانک میکند، embed میکند و به vector store میفرستد.
|
||||||
|
دیتای هر کاربر (sensor_uuid) جدا embed و با metadata ذخیره میشود.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
recreate: اگر True باشد، collection را از نو میسازد
|
recreate: اگر True باشد، collection را از نو میسازد
|
||||||
@@ -117,13 +117,17 @@ def ingest(recreate: bool = False, config: RAGConfig | None = None) -> dict:
|
|||||||
all_metas: list[dict] = []
|
all_metas: list[dict] = []
|
||||||
all_ids: list[str] = []
|
all_ids: list[str] = []
|
||||||
|
|
||||||
for source_id, content in sources:
|
for source_id, content, sensor_uuid in sources:
|
||||||
chunks = chunk_text(content, config=cfg)
|
chunks = chunk_text(content, config=cfg)
|
||||||
for i, ch in enumerate(chunks):
|
for i, ch in enumerate(chunks):
|
||||||
uid = str(uuid.uuid4())
|
uid = str(uuid.uuid4())
|
||||||
all_ids.append(uid)
|
all_ids.append(uid)
|
||||||
all_chunks.append(ch)
|
all_chunks.append(ch)
|
||||||
all_metas.append({"source": source_id, "chunk_index": i})
|
all_metas.append({
|
||||||
|
"source": source_id,
|
||||||
|
"chunk_index": i,
|
||||||
|
"sensor_uuid": sensor_uuid,
|
||||||
|
})
|
||||||
|
|
||||||
if not all_chunks:
|
if not all_chunks:
|
||||||
return {"chunks_added": 0, "sources": [s[0] for s in sources], "error": "هیچ چانکی ساخته نشد"}
|
return {"chunks_added": 0, "sources": [s[0] for s in sources], "error": "هیچ چانکی ساخته نشد"}
|
||||||
|
|||||||
@@ -8,12 +8,17 @@ from .vector_store import QdrantVectorStore
|
|||||||
|
|
||||||
def search_with_query(
|
def search_with_query(
|
||||||
query: str,
|
query: str,
|
||||||
|
sensor_uuid: str,
|
||||||
limit: int = 5,
|
limit: int = 5,
|
||||||
score_threshold: float | None = None,
|
score_threshold: float | None = None,
|
||||||
config: RAGConfig | None = None,
|
config: RAGConfig | None = None,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
کوئری را embed میکند و در vector store جستجو میکند.
|
کوئری را embed میکند و در vector store جستجو میکند.
|
||||||
|
فقط chunks مربوط به sensor_uuid یا __global__ برمیگردد (ایزولهسازی کاربر).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sensor_uuid: شناسه سنسور کاربر — اجباری برای امنیت
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
لیست نتایج با id, score, text, metadata
|
لیست نتایج با id, score, text, metadata
|
||||||
@@ -25,4 +30,5 @@ def search_with_query(
|
|||||||
query_vector=query_vector,
|
query_vector=query_vector,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
score_threshold=score_threshold,
|
score_threshold=score_threshold,
|
||||||
|
sensor_uuid=sensor_uuid,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
"""
|
||||||
|
تسکهای Celery برای RAG
|
||||||
|
"""
|
||||||
|
from config.celery import app
|
||||||
|
|
||||||
|
from .ingest import ingest
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def rag_ingest_task(recreate: bool = True):
|
||||||
|
"""
|
||||||
|
embed و ذخیره دیتای همه کاربران در Qdrant.
|
||||||
|
هر چند ساعت یکبار اجرا شود (از طریق Celery Beat).
|
||||||
|
recreate=True: collection از نو ساخته میشود تا دیتای قدیمی حذف شود.
|
||||||
|
"""
|
||||||
|
result = ingest(recreate=recreate)
|
||||||
|
return result
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
"""
|
||||||
|
ساخت دیتای خاک کاربر از sensor_data و soil_data — Schema-agnostic
|
||||||
|
هر سنسور = یک کاربر. شناسایی با uuid_sensor.
|
||||||
|
|
||||||
|
مدلهای Django داخل توابع import میشوند تا از AppRegistryNotReady جلوگیری شود.
|
||||||
|
"""
|
||||||
|
from django.db.models import Model
|
||||||
|
|
||||||
|
|
||||||
|
# فیلدهایی که در متن embed نباید بیایند (شناسهها، رابطهها)
|
||||||
|
EXCLUDE_FIELD_NAMES = {"id", "created_at", "updated_at", "task_id", "recorded_at"}
|
||||||
|
|
||||||
|
|
||||||
|
def _model_to_data_fields(instance: Model, exclude: set[str] | None = None) -> dict:
|
||||||
|
"""
|
||||||
|
استخراج فیلدهای داده از یک instance با استفاده از introspection.
|
||||||
|
تغییرات بعدی در مدل باعث شکستن نمیشود.
|
||||||
|
"""
|
||||||
|
exclude = exclude or set()
|
||||||
|
out: dict = {}
|
||||||
|
for f in instance._meta.get_fields():
|
||||||
|
if f.many_to_many or f.one_to_many or f.one_to_one and f.auto_created:
|
||||||
|
continue
|
||||||
|
if f.name in exclude or f.name in EXCLUDE_FIELD_NAMES:
|
||||||
|
continue
|
||||||
|
if hasattr(f, "related_model") and f.related_model:
|
||||||
|
continue # FK
|
||||||
|
try:
|
||||||
|
val = getattr(instance, f.name, None)
|
||||||
|
if val is not None:
|
||||||
|
out[f.name] = val
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def build_user_soil_text(sensor_uuid: str) -> str | None:
|
||||||
|
"""
|
||||||
|
ساخت متن قابل embed برای یک سنسور (کاربر).
|
||||||
|
از SensorData → SoilLocation → SoilDepthData خوانده میشود.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
متن متنی قابل چانک، یا None اگر سنسور یافت نشد.
|
||||||
|
"""
|
||||||
|
from sensor_data.models import SensorData
|
||||||
|
from soil_data.models import SoilDepthData
|
||||||
|
|
||||||
|
try:
|
||||||
|
sensor = SensorData.objects.select_related("location").get(
|
||||||
|
uuid_sensor=sensor_uuid
|
||||||
|
)
|
||||||
|
except SensorData.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
parts: list[str] = []
|
||||||
|
|
||||||
|
# شناسه سنسور
|
||||||
|
parts.append(f"سنسور: {sensor.uuid_sensor}")
|
||||||
|
|
||||||
|
# موقعیت مزرعه
|
||||||
|
loc = sensor.location
|
||||||
|
parts.append(
|
||||||
|
f"موقعیت مزرعه: عرض {loc.latitude}، طول {loc.longitude}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# خوانشهای سنسور (schema-agnostic)
|
||||||
|
sensor_fields = _model_to_data_fields(
|
||||||
|
sensor, exclude={"uuid_sensor", "location_id", "location"}
|
||||||
|
)
|
||||||
|
if sensor_fields:
|
||||||
|
sensor_lines = [f" {k}: {v}" for k, v in sorted(sensor_fields.items())]
|
||||||
|
parts.append("خوانشهای سنسور:\n" + "\n".join(sensor_lines))
|
||||||
|
|
||||||
|
# دادههای خاک به تفکیک عمق
|
||||||
|
depths = (
|
||||||
|
SoilDepthData.objects.filter(soil_location=loc)
|
||||||
|
.order_by("depth_label")
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
if depths:
|
||||||
|
depth_parts = []
|
||||||
|
for d in depths:
|
||||||
|
d_data = _model_to_data_fields(
|
||||||
|
d, exclude={"soil_location", "soil_location_id"}
|
||||||
|
)
|
||||||
|
if d_data:
|
||||||
|
lines = [f" {k}: {v}" for k, v in sorted(d_data.items())]
|
||||||
|
depth_parts.append(f" عمق {d.depth_label}:\n" + "\n".join(lines))
|
||||||
|
if depth_parts:
|
||||||
|
parts.append("دادههای خاک:\n" + "\n".join(depth_parts))
|
||||||
|
|
||||||
|
return "\n\n".join(parts) if parts else None
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_sensor_uuids() -> list[str]:
|
||||||
|
"""لیست همه uuid_sensor های موجود."""
|
||||||
|
from sensor_data.models import SensorData
|
||||||
|
|
||||||
|
return [
|
||||||
|
str(u) for u in
|
||||||
|
SensorData.objects.values_list("uuid_sensor", flat=True).distinct()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def load_user_sources() -> list[tuple[str, str]]:
|
||||||
|
"""
|
||||||
|
بارگذاری منابع دیتای کاربران از DB.
|
||||||
|
Returns: [(source_id, content), ...]
|
||||||
|
source_id = user:{sensor_uuid}
|
||||||
|
"""
|
||||||
|
uuids = get_all_sensor_uuids()
|
||||||
|
sources: list[tuple[str, str]] = []
|
||||||
|
for uid in uuids:
|
||||||
|
text = build_user_soil_text(str(uid))
|
||||||
|
if text and text.strip():
|
||||||
|
sources.append((f"user:{uid}", text))
|
||||||
|
return sources
|
||||||
@@ -95,16 +95,34 @@ class QdrantVectorStore:
|
|||||||
query_vector: list[float],
|
query_vector: list[float],
|
||||||
limit: int = 5,
|
limit: int = 5,
|
||||||
score_threshold: float | None = None,
|
score_threshold: float | None = None,
|
||||||
|
sensor_uuid: str | None = None,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
جستجوی شباهت بر اساس query vector.
|
جستجوی شباهت بر اساس query vector.
|
||||||
از query_points استفاده میکند (qdrant-client >= 2.0).
|
از query_points استفاده میکند (qdrant-client >= 2.0).
|
||||||
|
sensor_uuid: اجباری — فقط chunks مربوط به این سنسور یا __global__ برگردانده میشود.
|
||||||
"""
|
"""
|
||||||
|
query_filter = None
|
||||||
|
if sensor_uuid:
|
||||||
|
query_filter = qmodels.Filter(
|
||||||
|
should=[
|
||||||
|
qmodels.FieldCondition(
|
||||||
|
key="sensor_uuid",
|
||||||
|
match=qmodels.MatchValue(value=sensor_uuid),
|
||||||
|
),
|
||||||
|
qmodels.FieldCondition(
|
||||||
|
key="sensor_uuid",
|
||||||
|
match=qmodels.MatchValue(value="__global__"),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
response = self.client.query_points(
|
response = self.client.query_points(
|
||||||
collection_name=self.qdrant.collection_name,
|
collection_name=self.qdrant.collection_name,
|
||||||
query=query_vector,
|
query=query_vector,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
score_threshold=score_threshold,
|
score_threshold=score_threshold,
|
||||||
|
query_filter=query_filter,
|
||||||
)
|
)
|
||||||
points = getattr(response, "points", []) or []
|
points = getattr(response, "points", []) or []
|
||||||
|
|
||||||
|
|||||||
+18
-3
@@ -13,11 +13,15 @@ from .chat import chat_rag_stream
|
|||||||
class ChatView(APIView):
|
class ChatView(APIView):
|
||||||
"""
|
"""
|
||||||
چت RAG با استریم.
|
چت RAG با استریم.
|
||||||
POST با {"message": "متن سوال"} یا query param message
|
POST با {"message": "متن سوال", "sensor_uuid": "uuid-سنسور"}
|
||||||
|
sensor_uuid اجباری — هر کاربر فقط به دیتای خودش دسترسی دارد.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def post(self, request: Request):
|
def post(self, request: Request):
|
||||||
message = request.data.get("message") or request.query_params.get("message")
|
data = request.data if request.method == "POST" else request.query_params
|
||||||
|
message = data.get("message")
|
||||||
|
sensor_uuid = data.get("sensor_uuid")
|
||||||
|
|
||||||
if not message or not isinstance(message, str):
|
if not message or not isinstance(message, str):
|
||||||
return Response(
|
return Response(
|
||||||
{"code": 400, "msg": "پارامتر message الزامی است."},
|
{"code": 400, "msg": "پارامتر message الزامی است."},
|
||||||
@@ -29,10 +33,21 @@ class ChatView(APIView):
|
|||||||
{"code": 400, "msg": "پیام نباید خالی باشد."},
|
{"code": 400, "msg": "پیام نباید خالی باشد."},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
if not sensor_uuid or not isinstance(sensor_uuid, str):
|
||||||
|
return Response(
|
||||||
|
{"code": 400, "msg": "پارامتر sensor_uuid الزامی است."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
sensor_uuid = str(sensor_uuid).strip()
|
||||||
|
if not sensor_uuid:
|
||||||
|
return Response(
|
||||||
|
{"code": 400, "msg": "sensor_uuid نباید خالی باشد."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
def generate():
|
def generate():
|
||||||
try:
|
try:
|
||||||
for chunk in chat_rag_stream(message):
|
for chunk in chat_rag_stream(message, sensor_uuid=sensor_uuid):
|
||||||
yield chunk
|
yield chunk
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
yield f"\n[خطا: {e}]"
|
yield f"\n[خطا: {e}]"
|
||||||
|
|||||||
Reference in New Issue
Block a user