|
|
@@ -0,0 +1,212 @@
|
|
|
+# file: src/generate_answer.py
|
|
|
+import os
|
|
|
+import json
|
|
|
+import requests
|
|
|
+from typing import List, Dict
|
|
|
+from dotenv import load_dotenv
|
|
|
+
|
|
|
+load_dotenv()
|
|
|
+
|
|
|
+LLAMA_CPP_URL = os.getenv("LLAMA_CPP_URL", "http://localhost:8070/completion")
|
|
|
+
|
|
|
+def deduplicate_chunks(chunks: List[Dict]) -> List[Dict]:
|
|
|
+ """Eemaldab duplikaadi chunk'id."""
|
|
|
+ seen = set()
|
|
|
+ unique_chunks = []
|
|
|
+
|
|
|
+ for chunk in chunks:
|
|
|
+ key = (chunk['filename'], chunk['page'], chunk['text'][:50])
|
|
|
+ if key not in seen:
|
|
|
+ seen.add(key)
|
|
|
+ unique_chunks.append(chunk)
|
|
|
+
|
|
|
+ return unique_chunks
|
|
|
+
|
|
|
+def build_context(articles: List[Dict], chunks: List[Dict], max_chars: int = 8000) -> str:
|
|
|
+ """
|
|
|
+ Suurendatud kontekst. Weaviate andmete osa oluliselt suurem.
|
|
|
+ """
|
|
|
+ context_parts = []
|
|
|
+ char_count = 0
|
|
|
+
|
|
|
+ unique_chunks = deduplicate_chunks(chunks)
|
|
|
+
|
|
|
+ # SUURENDATUD: Weaviate artikli metaandmed
|
|
|
+ context_parts.append("=== ARTICLE SUMMARIES FROM WEAVIATE ===\n")
|
|
|
+ for i, art in enumerate(articles[:8], 1): # 5 → 8 artiklit!
|
|
|
+
|
|
|
+ # Koosta rikkalik artikli info
|
|
|
+ authors_str = ', '.join(art['authors'][:3]) if art['authors'] else 'N/A'
|
|
|
+ keywords_str = ', '.join(art['key_concepts'][:6]) if art['key_concepts'] else 'N/A'
|
|
|
+ methods_str = ', '.join(art['methods_used'][:4]) if art.get('methods_used') else 'N/A'
|
|
|
+
|
|
|
+ # Parsime transport_context (JSON string)
|
|
|
+ transport_str = "N/A"
|
|
|
+ try:
|
|
|
+ if art.get('transport_context'):
|
|
|
+ import json
|
|
|
+ tc = json.loads(art['transport_context'])
|
|
|
+ theoretical = tc.get('theoretical_contribution', '')
|
|
|
+ practical = tc.get('practical_applicability', '')
|
|
|
+ transport_str = f"Theoretical: {theoretical[:200]}... Practical: {practical[:200]}..."
|
|
|
+ except:
|
|
|
+ transport_str = art.get('transport_context', '')[:300]
|
|
|
+
|
|
|
+ article_text = f"""[Article {i}] {art['title']}
|
|
|
+Authors: {authors_str}
|
|
|
+Year: {art.get('year', 'N/A')}
|
|
|
+Keywords: {keywords_str}
|
|
|
+Methods Used: {methods_str}
|
|
|
+Transport Context: {transport_str}
|
|
|
+Summary (Estonian): {art['summary_et'][:900]} ← 350 → 900 märki!
|
|
|
+
|
|
|
+"""
|
|
|
+ context_parts.append(article_text)
|
|
|
+ char_count += len(article_text)
|
|
|
+
|
|
|
+ if char_count > max_chars * 0.45: # 45% artiklitele
|
|
|
+ break
|
|
|
+
|
|
|
+ # pgvector excerpts (sama mis enne)
|
|
|
+ context_parts.append("\n=== TEXT EXCERPTS FROM SOURCES ===\n")
|
|
|
+ for i, chunk in enumerate(unique_chunks[:15], 1):
|
|
|
+ chunk_text = f"""[Excerpt {i}] ({chunk['filename']}, page {chunk['page']})
|
|
|
+{chunk['text'][:280]}
|
|
|
+
|
|
|
+"""
|
|
|
+ context_parts.append(chunk_text)
|
|
|
+ char_count += len(chunk_text)
|
|
|
+
|
|
|
+ if char_count > max_chars:
|
|
|
+ break
|
|
|
+
|
|
|
+ return "\n".join(context_parts)
|
|
|
+
|
|
|
+def generate_answer(query: str, context: str, temperature: float = 0.6, max_tokens: int = 1500) -> str:
|
|
|
+ """
|
|
|
+ Genereerib vastuse. Sama kui enne, aga kontekst on rikkam.
|
|
|
+ """
|
|
|
+
|
|
|
+ prompt = f"""You are an expert in scientific research on transportation safety and traffic engineering.
|
|
|
+
|
|
|
+CONTEXT (from scientific sources in Weaviate database):
|
|
|
+{context}
|
|
|
+
|
|
|
+USER QUESTION (in Estonian):
|
|
|
+{query}
|
|
|
+
|
|
|
+INSTRUCTIONS:
|
|
|
+- Answer in fluent Estonian language
|
|
|
+- Use data and findings from the sources provided above
|
|
|
+- Cite articles or excerpts by number in square brackets (e.g., "[Article 1]" or "[Excerpt 3]")
|
|
|
+- Be concrete, detailed, and factual
|
|
|
+- Write at least 250 words (important: use the context fully)
|
|
|
+- Do NOT speculate or add information not present in the context
|
|
|
+- Reference the methods, findings, and practical applications mentioned in the articles
|
|
|
+- Structure your answer clearly with paragraphs
|
|
|
+
|
|
|
+ANSWER (in Estonian):"""
|
|
|
+
|
|
|
+ payload = {
|
|
|
+ "prompt": prompt,
|
|
|
+ "n_predict": max_tokens,
|
|
|
+ "temperature": temperature,
|
|
|
+ "top_p": 0.95,
|
|
|
+ "repeat_penalty": 1.2,
|
|
|
+ "presence_penalty": 0.1,
|
|
|
+ "frequency_penalty": 0.1,
|
|
|
+ }
|
|
|
+
|
|
|
+ print(f"📤 Saandan päring llama.cpp'le...")
|
|
|
+ print(f" 📊 Konteksti suurus: {len(context)} märki ≈ {len(context)//4} tokenit")
|
|
|
+
|
|
|
+ try:
|
|
|
+ response = requests.post(LLAMA_CPP_URL, json=payload, timeout=240)
|
|
|
+ response.raise_for_status()
|
|
|
+ result = response.json()
|
|
|
+
|
|
|
+ answer = result.get("content", "").strip()
|
|
|
+ tokens_predicted = result.get("tokens_predicted", 0)
|
|
|
+
|
|
|
+ print(f" ✅ Genereeriud: {tokens_predicted} tokenit, {len(answer.split())} sõna")
|
|
|
+
|
|
|
+ if not answer:
|
|
|
+ return "⚠️ LLM tagastas tühja vastuse."
|
|
|
+
|
|
|
+ word_count = len(answer.split())
|
|
|
+ if word_count < 100:
|
|
|
+ print(f" ⚠️ Liiga lühike ({word_count} sõna) – konteksti ei piisa")
|
|
|
+ return f"⚠️ LLM vastus on liiga lühike ({word_count} sõna):\n\n{answer}"
|
|
|
+
|
|
|
+ return answer
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ return f"❌ Viga: {e}"
|
|
|
+
|
|
|
+def rag_query(query: str, articles: List[Dict], chunks: List[Dict],
|
|
|
+ temperature: float = 0.6, max_tokens: int = 1500) -> Dict:
|
|
|
+ """RAG tsükkel."""
|
|
|
+ print(f"\n{'='*70}")
|
|
|
+ print(f"🔍 RAG PÄRING")
|
|
|
+ print(f"{'='*70}")
|
|
|
+ print(f"Küsimus: {query}")
|
|
|
+ print(f"Artikleid: {len(articles)}, Chunk'e: {len(chunks)}")
|
|
|
+
|
|
|
+ context = build_context(articles, chunks, max_chars=8000) # 5000 → 8000
|
|
|
+ answer = generate_answer(query, context, temperature=temperature, max_tokens=max_tokens)
|
|
|
+
|
|
|
+ # DEBUG: salvesta kontekst
|
|
|
+ with open("/tmp/rag_context_debug.txt", "w") as f:
|
|
|
+ f.write(context)
|
|
|
+
|
|
|
+ return {
|
|
|
+ "query": query,
|
|
|
+ "answer": answer,
|
|
|
+ "sources": {
|
|
|
+ "articles": [
|
|
|
+ {
|
|
|
+ "title": a["title"],
|
|
|
+ "authors": a.get("authors", [])[:3],
|
|
|
+ "score": a.get("score", 0)
|
|
|
+ }
|
|
|
+ for a in articles[:8] # 5 → 8
|
|
|
+ ],
|
|
|
+ "chunks_used": len(set((c['filename'], c['page']) for c in chunks[:15]))
|
|
|
+ },
|
|
|
+ "parameters": {
|
|
|
+ "context_chars": len(context),
|
|
|
+ "context_tokens_approx": len(context) // 4,
|
|
|
+ "context_weaviate_chars": len("\n".join([a['summary_et'][:900] for a in articles[:8]])),
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ from src.query_hybrid import hybrid_search
|
|
|
+
|
|
|
+ query = "young driver accident risk"
|
|
|
+ query = "traffic flow"
|
|
|
+ query = "road safety"
|
|
|
+ print(f"\n🚀 Käivitan hübriidotsingu...")
|
|
|
+
|
|
|
+ results = hybrid_search(query, top_articles=10, top_chunks=20)
|
|
|
+
|
|
|
+ print(f"\n🤖 Genereerin RAG vastust...")
|
|
|
+ rag_result = rag_query(
|
|
|
+ query=results["query"],
|
|
|
+ articles=results["articles"],
|
|
|
+ chunks=results["chunks"],
|
|
|
+ temperature=0.6,
|
|
|
+ max_tokens=1500
|
|
|
+ )
|
|
|
+
|
|
|
+ print("\n" + "=" * 70)
|
|
|
+ print("🎯 RAG VASTUS:")
|
|
|
+ print("=" * 70)
|
|
|
+ print(rag_result["answer"])
|
|
|
+
|
|
|
+ print("\n" + "=" * 70)
|
|
|
+ print("📊 STATISTIKA:")
|
|
|
+ print("=" * 70)
|
|
|
+ print(f"Weaviate konteksti: {rag_result['parameters']['context_weaviate_chars']} märki")
|
|
|
+ print(f"Kokku konteksti: {rag_result['parameters']['context_chars']} märki")
|
|
|
+ print(f"Kokku tokenit: ~{rag_result['parameters']['context_tokens_approx']}")
|