## Summary - Closes Phases 1–3 of [#62](#62) (Phase 4 eval harness left for a follow-up). - **Accuracy:** Retrieval is decided every turn (`GroundingDecider`, fails open). `FAST` no longer skips search — it only selects `OLLAMA_MODEL_FAST`. Search failures surface an explicit error instead of hallucinating from parametric memory. - **Search:** Pluggable `services/search/` with **SearxNG primary** + DDGS failover, ranking/dedupe/rumour filtering, numbered dated source blocks, citations persisted on `Prompt.citations` and emitted as `{"v":1,"type":"citations",...}` after stream end. - **Models:** Role-scoped `OLLAMA_MODEL_THINKING` / `_FAST` / `_UTILITY` / `OLLAMA_EMBED_MODEL=nomic-embed-text`, configurable `num_ctx`, real model name on `PromptMetric`, `reindex_embeddings` management command + loud embedding-dimension mismatch. ## SearxNG (ops) See README **SearxNG** section. Short version: run `searxng/searxng` on the GPU host, enable `json` in `settings.yml`, set `SEARXNG_BASE_URL=http://10.0.0.128:8080` in prod/beta secrets, open `:8080` on the LAN firewall like Ollama. ## Test plan - [x] `SKIP_RAG_INIT=1 python manage.py test chat_backend.tests` — 442 OK (6 skipped) - [ ] Deploy beta with updated secrets (`OLLAMA_MODEL_*`, `OLLAMA_EMBED_MODEL=nomic-embed-text`, `SEARXNG_BASE_URL`) - [ ] After embed change: `python manage.py reindex_embeddings` - [ ] Verify `did Taylor Swift get married` in FAST and THINKING returns grounded answer with citations frame - [ ] Kill SearxNG and confirm factual turns return search_unavailable (not Joe Alwyn hallucination); non-factual chat still worksReviewed-on: #65
This commit was merged in pull request #65.
This commit is contained in:
1 parent
57a2350b8e
commit
d8f5b8ebf2
32 files changed
+1734
-187
No files matched your search
@@ -11,7 +11,6 @@ from asgiref.sync import sync_to_async
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
from channels.db import database_sync_to_async
|
||||
from langchain_core.messages import HumanMessage, AIMessage, BaseMessage
|
||||
from langchain_community.tools import DuckDuckGoSearchRun
|
||||
from langgraph.graph import StateGraph, END
|
||||
|
||||
from .models import Conversation, Prompt, PromptMetric, DocumentWorkspace, CustomUser
|
||||
@@ -30,6 +29,8 @@ from .services.title_generator import title_generator
|
||||
from .services.moderation_classifier import moderation_classifier, ModerationLabel
|
||||
from .services.prompt_classifier.prompt_classifier import PromptClassifier, PromptType
|
||||
from .services.data_analysis_service import AsyncDataAnalysisService
|
||||
from .services.grounded_chat import citations_frame, prepare_grounded_chat
|
||||
from chat_backend.ollama_config import ollama_model_for_role, resolve_chat_role
|
||||
from .utils import (
|
||||
TokenUsageCollector,
|
||||
aiter_text_chunks,
|
||||
@@ -43,7 +44,6 @@ from finance.services.quotas import FeatureNotAllowed, QuotaExceeded, check_gene
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CHANNEL_NAME: str = "llm_messages"
|
||||
MODEL_NAME: str = "llama3.2"
|
||||
PROMPT_CLASSIFIER = PromptClassifier()
|
||||
|
||||
# --- Database Helpers (Reused) ---
|
||||
@@ -146,7 +146,7 @@ def get_messages(conversation_id, prompt, file_string: str = None, file_type: st
|
||||
return transformed_messages, prompt_instance
|
||||
|
||||
@database_sync_to_async
|
||||
def save_generated_message(conversation_id, message):
|
||||
def save_generated_message(conversation_id, message, citations=None):
|
||||
conversation = Conversation.objects.get(id=conversation_id)
|
||||
serializer = PromptSerializer(
|
||||
data={
|
||||
@@ -159,6 +159,8 @@ def save_generated_message(conversation_id, message):
|
||||
prompt_instance = serializer.save()
|
||||
prompt_instance.conversation_id = conversation.id
|
||||
prompt_instance.save()
|
||||
if citations is not None:
|
||||
Prompt.objects.filter(pk=prompt_instance.pk).update(citations=citations)
|
||||
else:
|
||||
print(serializer.errors)
|
||||
|
||||
@@ -221,6 +223,8 @@ class ChatState(TypedDict):
|
||||
error: Union[str, None]
|
||||
model_name: str
|
||||
chat_user: Any
|
||||
citations: List[Dict[str, Any]]
|
||||
resolved_model: str
|
||||
|
||||
|
||||
# --- LangGraph Nodes ---
|
||||
@@ -277,22 +281,7 @@ async def generation_node(state: ChatState) -> ChatState:
|
||||
}
|
||||
return {"response_generator": {"type": "text", "content": "Image Generation is not supported at this time, but it will be soon."}}
|
||||
|
||||
# Feature Flag: Internet Access
|
||||
if prompt_type == PromptType.SEARCH:
|
||||
# Check modelName first - if FAST, we skip search regardless of settings
|
||||
if state.get("model_name") == "FAST":
|
||||
pass
|
||||
elif getattr(settings, "ALLOW_INTERNET_ACCESS", False):
|
||||
try:
|
||||
search = DuckDuckGoSearchRun()
|
||||
search_results = search.run(state["message"])
|
||||
messages.append(HumanMessage(content=f"Search Results: {search_results}"))
|
||||
except Exception as e:
|
||||
logger.error(f"Search failed: {e}")
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
|
||||
# Feature Flag: Internet Access / always-on grounding handled below for chat.
|
||||
if prompt_type == PromptType.RAG:
|
||||
chat_user = state.get("chat_user")
|
||||
if chat_user is not None:
|
||||
@@ -318,10 +307,26 @@ async def generation_node(state: ChatState) -> ChatState:
|
||||
generator = service.generate_response(prompt_instance.message, decoded_file, file_type)
|
||||
return {"response_generator": generator}
|
||||
|
||||
else: # GENERAL_CHAT or others
|
||||
service = AsyncLLMService()
|
||||
generator = service.generate_response(messages, prompt_instance.message, conversation_id)
|
||||
return {"response_generator": generator}
|
||||
else:
|
||||
# GENERAL_CHAT / SEARCH / UNKNOWN — always-on grounding (#62).
|
||||
# FAST selects a smaller model; it no longer skips search.
|
||||
grounded = await prepare_grounded_chat(
|
||||
message=state["message"],
|
||||
messages=messages,
|
||||
model_name=state.get("model_name"),
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
if grounded.error:
|
||||
return {
|
||||
"response_generator": grounded.error,
|
||||
"citations": [],
|
||||
"resolved_model": grounded.model_name or "",
|
||||
}
|
||||
return {
|
||||
"response_generator": grounded.generator,
|
||||
"citations": grounded.citations,
|
||||
"resolved_model": grounded.model_name,
|
||||
}
|
||||
|
||||
|
||||
# --- LangGraph Definition ---
|
||||
@@ -475,12 +480,13 @@ class ChatConsumerGraph(AsyncWebsocketConsumer):
|
||||
if not decoded_file:
|
||||
decoded_file, file_type = await get_conversation_file_async(conversation_id)
|
||||
|
||||
resolved_model = ollama_model_for_role(resolve_chat_role(model))
|
||||
prompt_metric = await create_prompt_metric(
|
||||
prompt_instance.id,
|
||||
prompt_instance.message,
|
||||
True if file else False,
|
||||
file_type,
|
||||
MODEL_NAME,
|
||||
resolved_model,
|
||||
conversation_id,
|
||||
)
|
||||
|
||||
@@ -498,6 +504,8 @@ class ChatConsumerGraph(AsyncWebsocketConsumer):
|
||||
"error": None,
|
||||
"model_name": model,
|
||||
"chat_user": chat_user,
|
||||
"citations": [],
|
||||
"resolved_model": resolved_model,
|
||||
}
|
||||
print("Initial State: ", initial_state)
|
||||
|
||||
@@ -535,8 +543,21 @@ class ChatConsumerGraph(AsyncWebsocketConsumer):
|
||||
tokens_in, tokens_out = usage.pair
|
||||
|
||||
await self.send("END_OF_THE_STREAM_ENDER_GAME_42")
|
||||
|
||||
await save_generated_message(conversation_id, full_response)
|
||||
|
||||
citations = final_state.get("citations") or []
|
||||
if citations:
|
||||
await self.send_json_message(json.dumps(citations_frame(citations)))
|
||||
|
||||
final_model = final_state.get("resolved_model") or resolved_model
|
||||
if final_model and final_model != prompt_metric.model_name:
|
||||
prompt_metric.model_name = final_model
|
||||
await database_sync_to_async(prompt_metric.save)(
|
||||
update_fields=["model_name"]
|
||||
)
|
||||
|
||||
await save_generated_message(
|
||||
conversation_id, full_response, citations=citations
|
||||
)
|
||||
await finish_prompt_metric(
|
||||
prompt_metric,
|
||||
len(full_response),
|
||||
|
||||
Reference in new issue
Block a user