UPDATE
This commit is contained in:
+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
|
||||
|
||||
Reference in New Issue
Block a user