feat: add RAG quality metrics, optimized prompts, and evaluation tools

Optimized Prompts:
- Create utils/rag_prompt_builder.py with citation-optimized prompts
- Specialized for DAO tokenomics and technical documentation
- Proper citation format [1], [2] with doc_id, page, section
- Memory context integration (facts, events, summaries)
- Token count estimation

RAG Service Metrics:
- Add comprehensive logging in query_pipeline.py
- Log: question, doc_ids, scores, retrieval method, timing
- Track: retrieval_time, total_query_time, documents_found, citations_count
- Add metrics in ingest_pipeline.py: pages_processed, blocks_processed, pipeline_time

Router Improvements:
- Use optimized prompt builder in _handle_rag_query()
- Add graceful fallback: if RAG unavailable, use Memory only
- Log prompt token count, RAG usage, Memory usage
- Return detailed metadata (rag_used, memory_used, citations_count, metrics)

Evaluation Tools:
- Create tests/rag_eval.py for systematic quality testing
- Test fixed questions with expected doc_ids
- Save results to JSON and CSV
- Compare RAG Service vs Router results
- Track: citations, expected docs found, query times

Documentation:
- Create docs/RAG_METRICS_PLAN.md
- Plan for Prometheus metrics collection
- Grafana dashboard panels and alerts
- Implementation guide for metrics
This commit is contained in:
Apple
2025-11-16 05:12:19 -08:00
parent 382e661f1f
commit 1ed1181105
6 changed files with 769 additions and 57 deletions

169
utils/rag_prompt_builder.py Normal file
View File

@@ -0,0 +1,169 @@
"""
RAG Prompt Builder - optimized prompts for DAO tokenomics and documents
"""
from typing import List, Dict, Any, Optional
def build_rag_prompt_with_citations(
question: str,
memory_context: Dict[str, Any],
rag_citations: List[Dict[str, Any]],
rag_documents: Optional[List[Dict[str, Any]]] = None
) -> str:
"""
Build optimized prompt for RAG queries with citations
Optimized for:
- DAO tokenomics questions
- Technical documentation
- Multi-document answers with proper citations
Args:
question: User question
memory_context: Memory context (facts, events, summaries)
rag_citations: List of citations from RAG
rag_documents: Optional full documents (for context)
Returns:
Formatted prompt for LLM
"""
# Base system prompt
system_prompt = (
"Ти — експерт-консультант з токеноміки та архітектури DAO в екосистемі DAARION.city.\n"
"Твоя задача: дати чітку, структуровану відповідь на основі наданих документів та особистої пам'яті.\n\n"
"**Правила формування відповіді:**\n"
"1. Використовуй тільки інформацію з наданих документів та пам'яті\n"
"2. Посилайся на документи через індекси [1], [2], [3] тощо\n"
"3. Для технічних термінів (стейкінг, токени, ролі) давай конкретні приклади\n"
"4. Якщо в документах немає відповіді — чесно скажи, що не знаєш\n"
"5. Відповідай українською, структуровано (списки, абзаци)\n\n"
)
# Build Memory section
memory_section = _build_memory_section(memory_context)
# Build Documents section with citations
documents_section = _build_documents_section(rag_citations, rag_documents)
# Combine into final prompt
prompt_parts = [system_prompt]
if memory_section:
prompt_parts.append("**1. Особиста пам'ять та контекст:**\n")
prompt_parts.append(memory_section)
prompt_parts.append("\n")
if documents_section:
prompt_parts.append("**2. Релевантні документи DAO:**\n")
prompt_parts.append(documents_section)
prompt_parts.append("\n")
prompt_parts.append(f"**Питання користувача:**\n{question}\n\n")
prompt_parts.append("**Твоя відповідь (з цитатами [1], [2] тощо):**")
return "\n".join(prompt_parts)
def _build_memory_section(memory_context: Dict[str, Any]) -> str:
"""Build memory context section"""
parts = []
# User facts
facts = memory_context.get("facts", [])
if facts:
facts_list = []
for fact in facts[:5]: # Top 5 facts
key = fact.get("fact_key", "")
value = fact.get("fact_value", "")
if key and value:
facts_list.append(f"- {key}: {value}")
if facts_list:
parts.append("Особисті факти користувача:")
parts.extend(facts_list)
parts.append("")
# Recent events
events = memory_context.get("recent_events", [])
if events:
events_list = []
for event in events[:3]: # Last 3 events
body = event.get("body_text", "")
if body:
events_list.append(f"- {body[:150]}...")
if events_list:
parts.append("Останні події в діалозі:")
parts.extend(events_list)
parts.append("")
# Dialog summaries
summaries = memory_context.get("dialog_summaries", [])
if summaries:
summary_text = summaries[0].get("summary_text", "")
if summary_text:
parts.append(f"Підсумок попередніх діалогів: {summary_text[:200]}...")
return "\n".join(parts) if parts else ""
def _build_documents_section(
citations: List[Dict[str, Any]],
documents: Optional[List[Dict[str, Any]]] = None
) -> str:
"""
Build documents section with proper citation format
Format:
[1] (doc_id=microdao-tokenomics, page=1, section=Токеноміка):
MicroDAO використовує токен μGOV як ключ доступу...
"""
if not citations:
return "Документи не знайдено."
parts = []
for idx, citation in enumerate(citations[:5], start=1): # Top 5 citations
doc_id = citation.get("doc_id", "unknown")
page = citation.get("page", 0)
section = citation.get("section", "")
excerpt = citation.get("excerpt", "")
# Build citation header
header_parts = [f"[{idx}]"]
if doc_id != "unknown":
header_parts.append(f"doc_id={doc_id}")
if page:
header_parts.append(f"page={page}")
if section:
header_parts.append(f"section={section}")
header = " (" + ", ".join(header_parts) + "):"
# Add excerpt
if excerpt:
# Limit excerpt length
excerpt_clean = excerpt[:300] + "..." if len(excerpt) > 300 else excerpt
parts.append(f"{header}\n{excerpt_clean}")
else:
parts.append(f"{header}\n(фрагмент недоступний)")
parts.append("") # Empty line between citations
return "\n".join(parts)
def estimate_token_count(text: str, chars_per_token: float = 4.0) -> int:
"""
Rough estimate of token count
Args:
text: Text to estimate
chars_per_token: Average characters per token (default 4.0 for most models)
Returns:
Estimated token count
"""
return int(len(text) / chars_per_token)