Ga naar hoofdinhoud

Jan-V1: Een gids met demoproject

Leer hoe je een Deep Research Assistant bouwt met de agentische redeneercapaciteiten van Jan-v1, inclusief lokale deployment, Streamlit-appontwikkeling en meer.
· 12 min lezen

Verken met AI

Het Jan-team heeft onlangs Jan-v1 uitgebracht, een geavanceerd agentisch redeneermodel dat verfijnde onderzoeksautomatisering introduceert via verbeterd gebruik van tools en meerstaps probleemoplossing.

In deze tutorial focus ik op de unieke agentische redeneercapaciteiten van Jan-v1 en laat ik zien hoe het fungeert als een complex onderzoeksworkflow via een lokaal gedeployde Streamlit-interface, aangestuurd door llama.cpp.

In deze tutorial leg ik uit hoe je:

  • Jan-v1 lokaal deployt met GGUF-kwantisatie voor optimale prestaties en privacy
  • Een Streamlit-app bouwt voor geautomatiseerd onderzoek met realtime voortgangsweergave
  • Intelligente webzoekintegratie implementeert met dynamische querygeneratie
  • Een professionele rapportsynthese maakt met formatspecifieke outputstructurering
  • Geavanceerde tekstverwerking toepast om schone, business-ready documentatie te garanderen

Aan het einde ziet je app er zo uit:

Deep Research Assistant Demo

Wat is Jan-v1?

Jan-v1 is een model met 4B-parameters, gebouwd op de Lucy- en Qwen3-4B-thinking-basis, ontworpen voor agentische workflows zoals toolgebruik, meerstaps redeneren en geautomatiseerd onderzoek. In tegenstelling tot conventionele LLM’s blinkt Jan-v1 uit in het coördineren tussen tools, databronnen en redeneerstappen via gestructureerde scaffolding en op gedrag afgestemde beloningen.

Enkele kernfeatures van dit model:

  • Agent-ready architectuur: Dit model is geoptimaliseerd voor integratie in de praktijk en verwerkt webzoekopdrachten, synthese en gestructureerde rapportgeneratie met verbeterd redeneren en tool-API’s.
  • Efficiëntie: Ondanks het kleine formaat levert Jan-v1 prestaties die je normaal bij veel grotere modellen ziet, dankzij gerichte RL-training die focust op doelgerichte output.
  • Privacy-first deployment: Het compacte GGUF-formaat maakt snelle, lokale inferentie mogelijk, ideaal voor privé onderzoekstaken met lage latentie op laptops of CPU’s.
  • Op onderzoek afgestemde training: Jan-v1 gebruikt nieuwe beloningsfuncties die informativiteit en beknoptheid balanceren — cruciaal voor taken met hoge inzet zoals analyse en rapportage — waardoor hallucinaties en breedsprakigheid worden geminimaliseerd.
  • Gebouwd op Qwen3-4B-thinking: Dit model erft robuust chain-of-thought-redeneren en toolbediening van Qwen3’s thinking-modusfusie, wat het bijzonder effectief maakt voor geautomatiseerde onderzoekspijplijnen.

Jan-v1 lokaal instellen

Voor dit project deployen we Jan-v1 lokaal met Llama.cpp en de gekwantiseerde GGUF-versie voor optimale prestaties en middelengebruik.

Stap 1: Installeer de dependencies

Zorg eerst dat je de vereiste Python-packages hebt:

pip install llama-cpp-python==0.3.15
pip install streamlit aiohttp 

Deze opdracht installeert alle kerndependencies die nodig zijn voor AI-modelintegratie, het bouwen van een webinterface en asynchrone webbewerkingen.

Stap 2: Download de Jan-v1 GGUF-versie

Maak voordat je het model downloadt een nieuwe map models/ met het volgende commando:

 mkdir -p models

Download daarna het model met het volgende commando:

curl -L https://huggingface.co/janhq/Jan-v1-4B-GGUF/resolve/main/Jan-v1-4B-Q4_K_M.gguf -o Jan-v1-4B-Q4_K_M.gguf

Na deze stap heb je het Jan-v1 4B-model in GGUF-formaat succesvol gedownload en in de map models/ geplaatst. Deze gekwantiseerde versie biedt een goede balans tussen prestaties en geheugenefficiëntie, ideaal voor lokale inferentie op CPU’s of Colab.

Stap 3: Stel de Serper API-sleutel in

Meld je aan of log in bij je Serper-account: https://serper.dev/api-keys en kies het tabblad API Keys, kopieer vervolgens de standaard API-sleutel voor deze demo. Laten we deze sleutel als omgevingsvariabele instellen als volgt:

export SERPER_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxx

Serper API Key

Je bent nu klaar om het model te laden en ermee te werken in je research-assistantpipeline.

Demo: Deep Research Assistant met Jan-v1

Laten we een Streamlit-applicatie bouwen die de geavanceerde onderzoeksvaardigheden van Jan-v1 demonstreert, met intelligente querygeneratie, geautomatiseerd webzoeken (met Serper) en professionele rapportsynthese.

Stap 1: Importeer de libraries

We beginnen met het importeren van alle benodigde libraries voor onze research-assistant. Deze ondersteunen alles van het bouwen van de webinterface tot asynchroon webzoeken en lokale AI-modelintegratie.

import streamlit as st
import json
import time
import re
from datetime import datetime
from typing import List, Dict, Optional
import asyncio
import aiohttp
from llama_cpp import Llama
import os
from dataclasses import dataclass

We importeren alle kernlibraries die in de hele app worden gebruikt. Denk aan UI-componenten (Streamlit), concurrency-tools (asyncio, aiohttp) en de LLM-backend (llama_cpp). We nemen ook utilities mee voor parsen, typen en datumafhandeling.

Stap 2: Helperfuncties

Nu implementeren we een paar helperfuncties die rapportopmaak en inhoudsextractie afhandelen. Deze functies zorgen ervoor dat onze gegenereerde rapporten professionele kwaliteit en een consistente structuur behouden.

def sections_for_format(fmt: str) -> list[str]:
    fmt = (fmt or "").strip().lower()
    if fmt == "executive":
        return ["EXECUTIVE SUMMARY"]
    if fmt == "detailed":
        return [
            "INTRODUCTION",
            "DETAILED ANALYSIS",
            "CURRENT TRENDS AND DEVELOPMENTS",
            "IMPLICATIONS AND RECOMMENDATIONS",
            "CONCLUSION",
        ]
    if fmt == "academic":
        return [
            "ABSTRACT",
            "INTRODUCTION",
            "METHODOLOGY",
            "FINDINGS",
            "DISCUSSION",
            "CONCLUSION",
        ]
    if fmt == "presentation":
        return [
            "OVERVIEW",
            "KEY INSIGHTS",
            "RECOMMENDATIONS",
            "NEXT STEPS",
            "CONCLUSION",
        ]
    return ["INTRODUCTION", "DETAILED ANALYSIS", "CONCLUSION"]
def extract_final_block(text: str) -> str:
    m = re.search(r"<final>([\s\S]*?)</final>", text, flags=re.IGNORECASE)
    if m:
        cleaned_text = m.group(1).strip()
    else:
        cleaned_text = text
    preamble_patterns = [
        r"^(?:note:|okay,|hmm,|internal|let me|i (?:will|’ll)|as an ai|thinking|plan:|here is your report|the following is|i have prepared|i am presenting|based on the provided information|below is the report|i hope this meets your requirements|this report outlines|this is the final report).*?$",
        r"^(?:Here is the report|I have compiled the report|The report is provided below|This is the requested report).*?$", # More specific preambles
        r"^(?:Please find the report below|Here's the report).*?$"
    ]
    for pattern in preamble_patterns:
        cleaned_text = re.sub(pattern, "", cleaned_text, flags=re.IGNORECASE | re.MULTILINE).strip()
    cleaned_text = re.sub(r"(?m)^\s*[-*•]\s+", "", cleaned_text)
    cleaned_text = re.sub(r"[#`*_]{1,3}", "", cleaned_text)
    headers = [
        "EXECUTIVE SUMMARY","INTRODUCTION","DETAILED ANALYSIS","CURRENT TRENDS AND DEVELOPMENTS",
        "IMPLICATIONS AND RECOMMENDATIONS","CONCLUSION","ABSTRACT","METHODOLOGY","FINDINGS",
        "DISCUSSION","OVERVIEW","KEY INSIGHTS","RECOMMENDATIONS","NEXT STEPS"
    ]
    sorted_headers = sorted(headers, key=len, reverse=True)
    first_pos = -1
    for h in sorted_headers:
        match = re.search(r'\b' + re.escape(h) + r'\b', cleaned_text, flags=re.IGNORECASE)
        if match:
            if first_pos == -1 or match.start() < first_pos:
                first_pos = match.start()
    if first_pos >= 0:
        cleaned_text = cleaned_text[first_pos:].strip()
    return cleaned_text

Zo past elke functie in de pipeline:

  • sections_for_format()-functie: Deze functie genereert sectiekoppen die zijn toegesneden op het gekozen rapportformat (bijv. executive, detailed, academic). Zo voldoet elk rapport aan professionele standaarden en verwachtingen.
  • extract_final_block()-functie: Deze postprocessingstap isoleert de kern van het rapport. Hij extraheert tekst tussen <final>-tags en verwijdert inleidingen, stopzinnen en markdown-artefacten. Ook detecteert hij de vroegste standaardkop (bijv. "INTRODUCTION", "SUMMARY") om irrelevante inleidende tekst te schrappen, zodat de uiteindelijke output schoon, gestructureerd en klaar voor gebruik is.

Deze helperfuncties zijn cruciaal om consistente, hoogwaardige output te behouden over verschillende rapportformaten en onderzoeksscenario’s voor een denkend model als Jan-v1.

Stap 3: De configuraties instellen

Laten we het kernconfiguratiesysteem opzetten dat modelparameters, API-instellingen en onderzoekspreferenties beheert. Deze modulaire aanpak zorgt voor eenvoudige aanpasbaarheid en optimale prestaties.

Stap 3.1: Onderzoeksconfiguratie

Deze stap definieert een configuratiesysteem en modellader voor efficiënte lokale inferentie met het Jan-v1-model. Het systeem ondersteunt reproduceerbare onderzoekstaken op omgevingen met alleen CPU, zoals laptops of Colab Pro, met behoud van flexibiliteit en veiligheid.

@dataclass
class ResearchConfig:
    model_path: str = "models/Jan-v1-4B-Q4_K_M.gguf" #change as per your location
    max_tokens: int = 2048
    temperature: float = 0.6
    top_p: float = 0.95
    top_k: int = 20
    context_length: int = 4096
    search_api_key: str = os.getenv("SERPER_API_KEY", "")
    search_engine: str = "serper"  

Hier is een overzicht van de configuratie uit de bovenstaande code:

  • Gecentraliseerde parameters: ResearchConfig is een @dataclass die alle LLM- en zoekmachineconfiguraties centraliseert, waardoor je eenvoudig kunt tunen voor verschillende onderzoeksscenario’s.
  • Inferentiehyperparameters: Parameters zoals temperature, top_p en top_k zijn ingesteld om creativiteit te balanceren met feitelijke onderbouwing — belangrijk voor onderzoeksgerichte outputs.
  • Geheugen en context: De context_length is ingesteld op 4096 tokens, geoptimaliseerd voor de langvormige redeneercapaciteiten van Jan-v1.
  • API-integratie: De search_api_key maakt integratie met Serper mogelijk voor realtime webzoek-augmentatie.

Stap 3.2: Deep Research-assistent 

In deze stap definiëren we een Deep Research-assistentklasse die fungeert als de kerncontroller voor alle onderzoekstaken. Zo worden low-level model- en confighandelingen geabstraheerd en worden high-level interfaces voor generatie, zoeken en synthese aangeboden.

class DeepResearchAssistant:
    def __init__(self, config: ResearchConfig):
        self.config = config
        self.llm = None
        self.demo_mode = False
    def load_model(self):
        try:
            if not os.path.exists(self.config.model_path):
                print(f"Model file not found: {self.config.model_path}")
                return False
            file_size_gb = os.path.getsize(self.config.model_path) / (1024**3)
            if file_size_gb < 1.0:
                print(f"Model file too small ({file_size_gb:.1f} GB).")
                return False
            self.llm = Llama(
                model_path=self.config.model_path,
                n_ctx=self.config.context_length,
                verbose=False,
                n_threads=max(1, min(4, os.cpu_count() // 2)),
                n_gpu_layers=0,
                use_mmap=True,
                use_mlock=False,
                n_batch=128,
                f16_kv=True,
            )
            test = self.llm("Hi", max_tokens=3, temperature=0.1, echo=False)
            ok = bool(test and 'choices' in test)
            print("Model loaded " if ok else "Model loaded but test generation failed ")
            return ok
        except Exception as e:
            print(f"Model loading failed: {e}")
            return False

De klasse DeepResearchAssistant is de hoofdorkestrator die het laden van het model, configuratievalidatie en een nette interface voor onderzoekstaken beheert. Dit configuratiesysteem biedt foutafhandeling en validatie, zodat het model correct wordt geladen en klaar is voor onderzoek. Belangrijkste features van deze klasse:

  • Model initialiseren: De methode load_model() initialiseert veilig het Jan-v1 GGUF-model via llama-cpp-python, geoptimaliseerd voor lokale CPU-inferentie.
  • Validatiechecks: Eerst verifiëren we het modelpad en de minimale bestandsgrootte om ontbrekende of corrupte bestanden te detecteren.
  • Memory mapping: Vervolgens gebruiken we use_mmap=True voor efficiënte, schijf-ondersteunde loading zonder het volledige RAM te gebruiken.
  • Geoptimaliseerde inferentie: We configureren ook threads, batchgrootte en zetten f16_kv=True aan voor lager geheugengebruik. Met n_gpu_layers=0 draait alles op de CPU.
  • Sanity check: We voeren een sanity-test uit met een lichte prompt na het laden om te bevestigen dat het model correct reageert.

Deze setup zorgt voor snelle, lichte lokale inferentie, ideaal om Jan-v1 op laptops of Colab te draaien zonder GPU’s.

Stap 3.3: Een reactie genereren

Vervolgens implementeren we het kernsysteem voor reactiegeneratie, samen met webzoekfunctionaliteit die onze onderzoeksautomatisering aandrijft. Hiervoor gebruiken we Serper-API-services zoals hieronder:

def generate_response(self, prompt: str, max_tokens: int = None, extra_stops: Optional[List[str]] = None) -> str:
        if not self.llm:
            return "Model not loaded."
        stops = ["</s>", "<|im_end|>", "<|endoftext|>"]
        if extra_stops:
            stops.extend(extra_stops)
        mt = max_tokens or self.config.max_tokens
        try:
            resp = self.llm(
                prompt,
                max_tokens=mt,
                temperature=self.config.temperature,
                top_p=self.config.top_p,
                top_k=self.config.top_k,
                stop=stops,
                echo=False
            )
            return resp["choices"][0]["text"].strip()
        except Exception as e:
            return f"Error generating response: {str(e)}"
    async def search_web(self, query: str, num_results: int = 10) -> List[Dict]:
        if self.config.search_api_key:
            return await self.search_serper(query, num_results)
        return []
    async def search_serper(self, query: str, num_results: int) -> List[Dict]:
        url = "https://google.serper.dev/search"
        payload = {"q": query, "num": num_results}
        headers = {"X-API-KEY": self.config.search_api_key, "Content-Type": "application/json"}
        async with aiohttp.ClientSession() as session:
            async with session.post(url, json=payload, headers=headers) as response:
                data = await response.json()
                results = []
                for item in data.get('organic', []):
                    results.append({
                        'title': item.get('title', ''),
                        'url': item.get('link', ''),
                        'snippet': item.get('snippet', ''),
                        'source': 'web'
                    })
                return results
    def generate_search_queries(self, topic: str, focus_area: str, depth: str) -> List[str]:
        counts = {'surface': 5, 'moderate': 8, 'deep': 15, 'comprehensive': 25}
        n = counts.get(depth, 8)
        base = [
            f"{topic} overview",
            f"{topic} recent developments",
            f"{topic} academic studies",
            f"{topic} case studies",
            f"{topic} policy and regulation",
            f"{topic} technical approaches",
            f"{topic} market analysis",
            f"{topic} statistics and data",
        ]
        return base[:n]

De bovenstaande code valt uiteen in de volgende kernfuncties:

  • generate_response()-functie: Deze functie handelt alle interacties met het Jan-v1-model af met correcte foutafhandeling en configureerbare stopsequenties. Dit borgt consistente, hoogwaardige tekstgeneratie voor onderzoeksynthese.
  • search_web() en search_serper()-functies: Deze functies implementeren asynchroon webzoeken met de Serper-API, waardoor snelle, parallelle zoekopdrachten over meerdere queries mogelijk zijn. Het async-ontwerp voorkomt UI-blokkering tijdens onderzoeksoperaties.
  • generate_search_queries()-functie: Om diverse zoekvragen te maken op basis van onderzoeksdiepte en focustopic, implementeren we de functie generate_search_queries(). Deze zorgt voor een brede dekking van het onderzoeksonderwerp — van overzichten en technische benaderingen tot casestudy’s, regelgeving en marktanalyse. De gebruiker kan het aantal queries opschalen van 5 tot 25 afhankelijk van de gekozen diepte (surface, moderate, deep of comprehensive).

Dit reactiegeneratiesysteem vormt de kern van onze onderzoeksautomatisering en combineert de redeneercapaciteiten van Jan-v1 met realtime webzoeken om onderzoeksrapporten te creëren.

Stap 3.4: Onderzoek synthetiseren

Vervolgens implementeren we een synthese-engine die ruwe zoekresultaten omzet in verzorgde, professionele rapporten.

def synthesize_research(self, topic: str, search_results: List[Dict], focus_area: str, report_format: str) -> str:
        context_lines = []
        for i, result in enumerate(search_results[:20]):
            title = result.get("title", "")
            snippet = result.get("snippet", "")
            context_lines.append(f"Source {i+1} Title: {title}\nSource {i+1} Summary: {snippet}")
        context = "\n".join(context_lines)
        sections = sections_for_format(report_format)
        sections_text = "\n".join(sections)
        synthesis_prompt = f"""
You are an expert research analyst. Write the final, polished report on: "{topic}" for a professional, real-world audience.
***CRITICAL INSTRUCTIONS:***
- Your entire response MUST be the final report, wrapped **EXACTLY** inside <final> and </final> tags.
- DO NOT output any text, thoughts, or commentary BEFORE the <final> tag or AFTER the </final> tag.
- DO NOT include any conversational filler, internal thoughts, or commentary about the generation process (e.g., "As an AI...", "I will now summarize...", "Here is your report:").
- DO NOT use markdown formatting (e.g., #, ##, *, -).
- DO NOT use bullet points or lists.
- Maintain a formal, academic/professional tone throughout.
- Ensure the report is complete and self-contained.
- Include the following section headers, in this order, and no others:
{sections_text}
Guidance:
- Base your writing strictly on the Research Notes provided below. If the notes lack specific data, write a careful, methodology-forward analysis without inventing facts or numbers.
Research Notes:
{context}
Now produce ONLY the final report:
<final>
...your report here...
</final>
"""
        raw = self.generate_response(synthesis_prompt, max_tokens=1800, extra_stops=["</final>"])
        final_report = extract_final_block(raw)
        final_report = re.sub(r"(?m)^\s*[-*•]\s+", "", final_report)
        final_report = re.sub(r"[#`*_]{1,3}", "", final_report)
        first = next((h for h in sections if h in final_report), None)
        if first:
            final_report = final_report[final_report.find(first):].strip()
        return final_report

De bovenstaande code benut de geavanceerde redeneercapaciteiten van Jan-v1 om ruwe webzoekresultaten om te zetten in professionele rapporten die normaal uren handmatig onderzoek en schrijven vergen. Laten we de belangrijkste mogelijkheden toelichten:

  • Contextuele inbedding: De code zet ongestructureerde zoekhits om in gestructureerde onderzoeksnotities door titels en samenvattingen in genummerde vorm te combineren. Dit zorgt voor hoge informatiedichtheid en minimaliseert contextfragmentatie wanneer het aan het model wordt doorgegeven.
  • Dynamische promptconstructie: Met de functie sections_for_format() genereren we sectiekoppen op maat van het gekozen outputformat, die met strikte generatieregels in de prompt worden geïnjecteerd om het modelgedrag te begrenzen.
  • Hallucinatiemitigatie: Door het gebruik van <final>...</final>-tags af te dwingen en conversatiescaffolding te verbieden, voorkomen we dat het model irrelevante tekst, speculatieve samenvattingen of overbodige kadering introduceert.
  • Postprocessing en normalisatie: Na decodering wordt het rapport opgeschoond met regex-filters om markdown-artefacten, lijstmarkeringen en losse opmaak te verwijderen. De functie zorgt er ook voor dat de output begint bij de eerste geldige sectiekop om de structurele consistentie te verbeteren.

Stap 4: Hoofdflow van de Streamlit-applicatie

Tot slot bouwen we de complete gebruikersinterface die al onze onderzoeksfunctionaliteit samenbrengt in een interactieve applicatie.

def main():
    st.set_page_config(page_title="Deep Research Assistant", page_icon="", layout="wide")
    st.markdown("""
    <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 1.25rem; border-radius: 10px; color: white; text-align: center; margin-bottom: 1rem;">
        <h1 style="margin:0;"> Deep Research Assistant</h1>
    </div>
    """, unsafe_allow_html=True)
    if 'research_assistant' not in st.session_state:
        config = ResearchConfig()
        st.session_state.research_assistant = DeepResearchAssistant(config)
        st.session_state.model_loaded = st.session_state.research_assistant.load_model()
        st.session_state.research_results = None
    if not st.session_state.research_assistant.config.search_api_key:
        st.warning("SERPER_API_KEY not found in environment. Using fallback demo results.")
    st.header("Research Configuration")
    research_topic = st.text_area(
        "Research Topic",
        placeholder="e.g., Impact of artificial intelligence on healthcare efficiency and patient outcomes",
        height=100
    )
    col1a, col1b = st.columns(2)
    with col1a:
        research_depth = st.selectbox(
            "Research Depth",
            ["surface", "moderate", "deep", "comprehensive"],
            index=1,
            format_func=lambda x: {
                "surface": "Surface (5-8 sources)",
                "moderate": "Moderate (10-15)",
                "deep": "Deep Dive (20-30)",
                "comprehensive": "Comprehensive (40+)"
            }[x]
        )
        focus_area = st.selectbox("Focus Area", ["general", "academic", "business", "technical", "policy"], index=0, format_func=str.title)
    with col1b:
        time_frame = st.selectbox(
            "Time Frame",
            ["current", "recent", "comprehensive"],
            index=1,
            format_func=lambda x: {"current":"Current (≤6 months)","recent":"Recent (≤2 years)","comprehensive":"All time"}[x]
        )
        report_format = st.selectbox(
            "Report Format",
            ["executive", "detailed", "academic", "presentation"],
            index=1,
            format_func=lambda x: {
                "executive":"Executive Summary",
                "detailed":"Detailed Analysis",
                "academic":"Academic Style",
                "presentation":"Presentation Format"
            }[x]
        )
    if st.button("Start Deep Research", type="primary", use_container_width=True):
        if not st.session_state.model_loaded:
            st.error("Model not loaded. See terminal logs for details.")
        elif not research_topic.strip():
            st.error("Please enter a research topic.")
        else:
            start_research(research_topic, research_depth, focus_area, time_frame, report_format)
    if st.session_state.research_results:
        display_research_results(st.session_state.research_results)
def start_research(topic: str, depth: str, focus: str, timeframe: str, format_type: str):
    assistant = st.session_state.research_assistant
    progress_bar = st.progress(0)
    status_text = st.empty()
    try:
        status_text.text("Generating search queries...")
        progress_bar.progress(15)
        queries = assistant.generate_search_queries(topic, focus, depth)
        status_text.text("Searching sources...")
        progress_bar.progress(40)
        all_results = []
        for i, query in enumerate(queries):
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            results = loop.run_until_complete(assistant.search_web(query, 5))
            all_results.extend(results)
            loop.close()
            progress_bar.progress(40 + int(((i + 1) / max(1, len(queries))) * 30))
            time.sleep(0.05)
        status_text.text("Synthesizing report...")
        progress_bar.progress(80)
        research_report = assistant.synthesize_research(topic, all_results, focus, format_type)
        status_text.text("Done.")
        progress_bar.progress(100)
        st.session_state.research_results = {
            'topic': topic,
            'report': research_report,
            'sources': all_results,
            'queries': queries,
            'config': {'depth': depth, 'focus': focus, 'timeframe': timeframe, 'format': format_type},
            'timestamp': datetime.now()
        }
        time.sleep(0.3)
        status_text.empty()
        progress_bar.empty()
    except Exception as e:
        st.error(f"Research failed: {str(e)}")
        status_text.empty()
        progress_bar.empty()
def display_research_results(results: Dict):
    st.header("Research Report")
    st.subheader(f"Topic: {results['topic']}")
    st.markdown(
        f'<div style="background:#f8f9ff;padding:1rem;border-radius:10px;border:1px solid #e1e8ed;">{results["report"]}</div>',
        unsafe_allow_html=True,
    )
    with st.expander("Sources", expanded=False):
        for i, source in enumerate(results['sources'][:12]):
            st.markdown(f"""
            <div style="background:#fff;padding:0.75rem;border-radius:8px;border:1px solid #e1e8ed;margin:0.4rem 0;">
                <h4 style="margin:0 0 .25rem 0;">{source['title']}</h4>
                <p style="margin:0 0 .25rem 0;">{source['snippet']}</p>
                <small><a href="{source['url']}" target="_blank">{source['url']}</a></small>
            </div>
            """, unsafe_allow_html=True)
    st.markdown("### Export")
    c1, c2, c3 = st.columns(3)
    with c1:
        report_text = f"Research Report: {results['topic']}\n\n{results['report']}"
        st.download_button("Download Text", data=report_text, file_name=f"research_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt", mime="text/plain")
    with c2:
        json_data = json.dumps(results, default=str, indent=2)
        st.download_button("Download JSON", data=json_data, file_name=f"research_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", mime="application/json")
    with c3:
        if st.button("Start New Research"):
            st.session_state.research_results = None
            st.experimental_rerun()
if __name__ == "__main__":
    main()

De bovenstaande code is opgedeeld in lagen, van het laden van het model en het verzamelen van gebruikersinput tot querygeneratie, webzoeken en uiteindelijke rapportsynthese. Laten we deze lagen bespreken:

  • UI-laag (Streamlit-frontend): De app heeft een header met gradientstijl en een responsieve lay-out voor een verzorgde gebruikerservaring. De interface blijft intuïtief terwijl je fijnmazige controle krijgt over onderzoeksparameters — ideaal voor zowel technische als niet-technische gebruikers.
  • Configureerbare pipeline: Gebruikers kunnen de workflow aanpassen met selectors voor diepte, focusgebied (general, academic, business), tijdsbestek en outputstijl (executive, academic, enz.).
  • Stateful agentbeheer: De DeepResearchAssistant wordt één keer per sessie geïnitialiseerd via st.session_state, zodat de modelstaat behouden blijft en herhaald laden wordt vermeden. 
  • Realtime feedback en async-zoekopdrachten: De voortgang wordt gevolgd met dynamische Streamlit-voortgangsbalken en statusupdates. Webzoeken wordt asynchroon uitgevoerd via asyncio, waardoor parallelle queries mogelijk zijn zonder de UI te blokkeren.
  • Query- en synthese-loop: Zoekqueries worden automatisch gegenereerd op basis van topic en diepte via een prompt-engineeringstrategie. Opgehaalde resultaten worden geaggregeerd en doorgegeven aan het Jan-v1-model voor gestructureerde rapportgeneratie.
  • Outputvisualisatie en export: Het eindrapport wordt weergegeven in een Markdown-blok met uitklapbare citaties. Gebruikers kunnen het rapport exporteren in TXT- of JSON-formaat of de workflow met één klik opnieuw uitvoeren.

Om het zelf te proberen, sla de code op als app.py en start:

streamlit run app.py

Conclusie

Deze Deep Research Assistant laat de praktische kracht zien van de geavanceerde redeneercapaciteiten van Jan-v1 bij het automatiseren van complexe onderzoeksworkflows. Het systeem combineert succesvol:

  • Intelligente automatisering: Maakt end-to-end onderzoek mogelijk van querygeneratie tot professionele rapportsynthese, en bespaart zo uren handwerk.
  • Schaalbaar ontwerp: De configureerbare onderzoeksdiepte past zich aan van snelle overzichten tot uitgebreide analyses, afhankelijk van de behoefte.
  • Professionele kwaliteit: Helpt bij het genereren van rapporten die geschikt zijn voor executive-briefings, academisch onderzoek en technische documentatie.
  • Toegankelijk voor gebruikers: Deze interface maakt geavanceerde AI-onderzoeksvaardigheden beschikbaar voor gebruikers ongeacht hun technische achtergrond.

De applicatie laat zien hoe de 91,1% SimpleQA-nauwkeurigheid van Jan-v1 en de verbeterde redeneercapaciteiten kunnen worden ingezet om productieklare tools te bouwen die direct waarde leveren in workflows voor onderzoek, analyse en contentcreatie.

Aashi Dutt

Auteur

Aashi Dutt

Ik ben een Google Developers Expert in ML (Gen AI), een Kaggle 3x Expert en een Women Techmakers Ambassador met meer dan 3 jaar ervaring in tech. In 2020 heb ik een healthtech-startup mee opgericht en ik volg een master computer science aan Georgia Tech, met als specialisatie machine learning.

Onderwerpen

Gerelateerd