import os import json import numpy as np import gradio as gr from huggingface_hub import hf_hub_download from sentence_transformers import SentenceTransformer from llama_cpp import Llama # ----------------------------- # 1. LLM NorwAI Mistral 7B via GGUF # ----------------------------- REPO_ID = "NorwAI/NorwAI-Mistral-7B-instruct" GGUF_FILENAME = "norwai-mistral-7b-instruct-q4_k_m.gguf" HF_TOKEN = os.environ.get("HF_TOKEN") if HF_TOKEN is None: raise RuntimeError( "HF_TOKEN environment variable is not set. " ) model_path = hf_hub_download( repo_id=REPO_ID, filename=GGUF_FILENAME, token=HF_TOKEN, ) llm = Llama( model_path=model_path, n_ctx=4096, n_threads=2, #12 verbose=False, ) # ----------------------------- # 2. Embedding model + knowledge base # ----------------------------- EMBED_MODEL_NAME = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" embedder = SentenceTransformer(EMBED_MODEL_NAME) KB_PATH = "kb_camhs_no.jsonl" if not os.path.exists(KB_PATH): raise RuntimeError(f"Knowledge base file {KB_PATH} not found in repository.") kb_docs = [] with open(KB_PATH, encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue kb_docs.append(json.loads(line)) kb_texts = [d["text"] for d in kb_docs] kb_embeddings = embedder.encode( kb_texts, convert_to_numpy=True, normalize_embeddings=True, ) # ----------------------------- # 3. Retrive and Output formatting # ----------------------------- def retrieve_relevant_chunks(query: str, k: int = 4): """Return top-k relevant KB entries for a Norwegian clinical query.""" q_emb = embedder.encode( [query], convert_to_numpy=True, normalize_embeddings=True, )[0] scores = kb_embeddings @ q_emb # cosine similarity (unit vectors) top_idx = np.argsort(-scores)[:k] return [kb_docs[i] for i in top_idx] def format_context(chunks): """Format retrieved chunks as numbered knowledge base blocks (for the prompt).""" lines = [] for i, ch in enumerate(chunks, start=1): src = ch.get("source_short", "Ukjent kilde") cit = ch.get("citation", "") url = ch.get("url", "") text = ch.get("text", "") block = f"[{i}] Kilde: {src}\n" if cit: block += f" Referanse: {cit}\n" if url: block += f" URL: {url}\n" block += f" Utdrag: {text}\n" lines.append(block) return "\n\n".join(lines) def format_references_for_display(chunks): """ Build a small, light-grey HTML block with the references that the user sees under the main answer. """ if not chunks: return "" ref_lines = [] for i, ch in enumerate(chunks, start=1): src = ch.get("source_short", "Ukjent kilde") cit = ch.get("citation", "") url = ch.get("url", "") # Keep it fairly short in the UI line = f"[{i}] {src}" if cit: line += f" – {cit}" if url: line += f" ({url})" ref_lines.append(line) refs_html = "
".join(ref_lines) styled_block = ( "
" "Kilder brukt i dette svaret:
" f"{refs_html}" "
" ) return styled_block SYSTEM_PROMPT = ( "Du er en norsk faglig assistent for klinikere i barne- og ungdomspsykiatrien (BUP) i Norge. " "Du skal gi korte, presise og faglig korrekte svar basert på gjeldende norske retningslinjer, " "veiledere, pakkeforløp og lovverk (for eksempel Helsedirektoratet, Helseregisterloven, " "Pasientjournalloven). Internasjonale kilder som WHO og NICE og forskningsartikler (for eksempel " "IDDEAS prosjekt og CAMHS-studier) kan brukes som sekundær støtte når norske retningslinjer ikke er dekkende, " "men skal da omtales som henholdsvis internasjonale anbefalinger eller forskningsfunn.\n\n" "Viktige prinsipper:\n" "- Svar alltid på norsk.\n" "- Ikke still diagnose, ikke foreslå konkrete medikamentdoser, og ikke gi individuell behandlingsplan basert på en anonym tekst.\n" "- Beskriv heller hva retningslinjer, pakkeforløp, lovverk og forskning sier om utredning, vurdering, samarbeid og ansvar.\n" "- Vær tydelig på at svaret er generell informasjon og ikke erstatter klinisk vurdering eller lokale prosedyrer.\n" "- Bruk kun kunnskapsgrunnlaget du har fått utdrag fra; hvis informasjon mangler, si at du er usikker eller at det ikke er tydelig beskrevet.\n" "- Når du bruker et utdrag, henvis til kilden med [1], [2] osv. i brødteksten, der [n] samsvarer med nummeret i kunnskapsgrunnlaget.\n" "- Skriv svaret som sammenhengende tekst uten egne overskrifter som 'Forklaring:' eller 'Svar:'.\n" "- Ikke gjenta samme setning flere ganger; skriv budskapet én gang, tydelig og kort.\n" ) def build_prompt(user_message: str, context_blocks): context_text = format_context(context_blocks) prompt = ( f"{SYSTEM_PROMPT}\n\n" "Nedenfor får du relevante utdrag fra norske og internasjonale kilder. Bruk dem aktivt i svaret ditt, " "og henvis i teksten med [1], [2] osv. der det passer.\n\n" "Kunnskapsgrunnlag:\n" f"{context_text}\n\n" "Oppgave:\n" f"Bruker: {user_message}\n" "Ikke gjenta samme setning to ganger.\n" "Assistent (Svar på norsk, rettet mot klinikere i Norge, svar direkte i løpende tekst, uten egne overskrifter som 'Forklaring:' eller 'Svar:'.)" ) return prompt def chat_fn(message, history): # 1) Retrieve relevant guideline and research papers chunks = retrieve_relevant_chunks(message, k=4) # 2) Build grounded prompt to the model prompt = build_prompt(message, chunks) # 3) LLM out = llm( prompt, max_tokens=256, temperature=0.4, top_p=0.9, stop=["Bruker:", "User:"], ) main_reply = out["choices"][0]["text"].strip() #4) Build small, light gray reference block below the answer refs_html = format_references_for_display(chunks) # Return main text + small gray reference part full_reply = main_reply + refs_html return full_reply demo = gr.ChatInterface( fn=chat_fn, title="Spør Datadrevet beslutningsstøtte for CAMHS Norway", description=( "Eksperimentell faglig assistent for barne- og ungdomspsykiatrien (BUP) i Norge. " "Svarene er generelle og basert på norske retningslinjer, pakkeforløp, lovverk og publisert forskning " "(inkludert IDDEAS prosjekt og norske CAMHS-studier), og kan ikke erstatte klinisk vurdering eller lokale prosedyrer." "\n\nGrunnleggende modell: **norwai-mistral-7b-instruct-q4_k_m.gguf**. fra NorwAI/NorwAI-Mistral-7B-instruct" ), ) if __name__ == "__main__": demo.launch()