Kendi Chatbot Uygulaması Geliştirme: Ollama ve Streamlit

Sunucu tarafında her şeyi kurdunuz, modeliniz çalışıyor, API’den güzel yanıtlar geliyor. Ama her seferinde terminal açıp curl komutları mı yazacaksınız? İşte tam bu noktada Streamlit devreye giriyor. Ollama ile yerel olarak çalışan bir LLM’i alıp, gerçek bir chatbot arayüzüne dönüştürmek aslında düşündüğünüzden çok daha az iş gerektiriyor.

Bu yazıda sıfırdan başlayıp, production’a yakın bir chatbot uygulaması geliştireceğiz. Sadece “hello world” seviyesinde değil, gerçekten kullanılabilir, özelleştirilebilir bir şey. Konuşma geçmişi, sistem promptu yönetimi, model seçimi, streaming yanıtlar… hepsini kapsayacağız.

Neden Streamlit?

Başka seçenekler de var tabii. Gradio var, Flask/FastAPI ile kendi UI’ınızı yazabilirsiniz, hatta Chainlit var. Ben birkaç projeyi farklı araçlarla denedim ve iç kullanım için, özellikle sysadmin ekiplerinin kendi araçlarını geliştirdiği senaryolarda Streamlit’in “en az sürtünme yaratan” çözüm olduğunu gördüm.

Neden mi? Çünkü Streamlit’te bir checkbox eklemek için frontend bilmenize gerek yok. State yönetimi için JavaScript yazmanıza gerek yok. Python biliyorsanız, bir günde çalışan bir uygulama çıkarabilirsiniz. Üstelik Streamlit uygulamaları container’lamak da son derece temiz.

Ortam Hazırlığı

Önce Ollama’nın çalıştığını varsayıyorum. Değilse önce onu kurun. Ardından Python ortamımızı hazırlayalım. Ben her proje için virtual environment kullanmayı şiddetle tavsiye ediyorum, sistem Python’unuzu kirletmemek için.

# Python virtual environment oluştur
python3 -m venv ollama-chat-env
source ollama-chat-env/bin/activate

# Gerekli paketleri yükle
pip install streamlit ollama requests python-dotenv

# Yüklü olduğunu doğrula
streamlit --version
python -c "import ollama; print('ollama paketi hazır')"

Proje yapımızı da baştan düzenli kuralım. İleride bakım yapmak için bu düzen çok işe yarıyor:

mkdir ollama-chatbot && cd ollama-chatbot
touch app.py config.py utils.py .env
mkdir -p .streamlit
touch .streamlit/config.toml

Streamlit’in tema ayarlarını da baştan yapalım, karanlık tema sysadmin’lerin gözüne daha iyi gelir genellikle:

cat > .streamlit/config.toml << 'EOF'
[theme]
base = "dark"
primaryColor = "#00b4d8"
backgroundColor = "#0d1117"
secondaryBackgroundColor = "#161b22"
textColor = "#c9d1d9"

[server]
port = 8501
headless = true
EOF

Temel Uygulama Yapısı

Şimdi asıl işe gelelim. Önce basit ama çalışan bir versiyon yazalım, sonra üstüne özellikler ekleyeceğiz. Bu yaklaşım her zaman daha sağlıklı.

# app.py - Temel versiyon
import streamlit as st
import ollama

st.set_page_config(
    page_title="Lokal AI Asistan",
    page_icon="🤖",
    layout="wide"
)

# Session state başlat
if "messages" not in st.session_state:
    st.session_state.messages = []

if "model" not in st.session_state:
    st.session_state.model = "llama3.2"

# Sidebar - model seçimi
with st.sidebar:
    st.title("⚙️ Ayarlar")
    
    # Mevcut modelleri çek
    try:
        models = ollama.list()
        model_names = [m['name'] for m in models['models']]
        st.session_state.model = st.selectbox(
            "Model Seç",
            model_names,
            index=0 if model_names else 0
        )
    except Exception as e:
        st.error(f"Ollama'ya bağlanılamadı: {e}")
        model_names = []

    if st.button("🗑️ Konuşmayı Temizle", use_container_width=True):
        st.session_state.messages = []
        st.rerun()

# Ana alan - mesaj geçmişini göster
for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

# Kullanıcı girişi
if prompt := st.chat_input("Bir şey sor..."):
    # Kullanıcı mesajını ekle
    st.session_state.messages.append({
        "role": "user",
        "content": prompt
    })
    
    with st.chat_message("user"):
        st.markdown(prompt)
    
    # Asistan yanıtı
    with st.chat_message("assistant"):
        response_placeholder = st.empty()
        full_response = ""
        
        # Streaming yanıt
        stream = ollama.chat(
            model=st.session_state.model,
            messages=st.session_state.messages,
            stream=True
        )
        
        for chunk in stream:
            content = chunk['message']['content']
            full_response += content
            response_placeholder.markdown(full_response + "▌")
        
        response_placeholder.markdown(full_response)
    
    st.session_state.messages.append({
        "role": "assistant",
        "content": full_response
    })

Bu kadar. streamlit run app.py dediğinizde tarayıcınızda çalışan bir chatbot göreceksiniz. Ama biz burada durmayacağız.

Sistem Promptu ve Kişiselleştirme

Gerçek kullanım senaryolarında sistem promptu kritik. Örneğin bu chatbot’u bir Linux troubleshooting asistanı olarak yapılandırmak istiyorsunuz. Ya da sadece belirli konularda yanıt vermesini istiyorsunuz. Bunu config.py dosyasına taşıyalım:

# config.py
SYSTEM_PROMPTS = {
    "Genel Asistan": "Sen yardımcı bir asistansın. Türkçe yanıt ver.",
    
    "Linux Uzmanı": """Sen deneyimli bir Linux sistem yöneticisisin. 
    Kullanıcılara Linux, shell scripting, sistem yönetimi ve 
    troubleshooting konularında yardım ediyorsun. 
    Yanıtlarında her zaman pratik komut örnekleri ver.
    Güvenlik best practice'lerini her zaman vurgula.
    Yanıtlarını Türkçe ver.""",
    
    "Kubernetes Asistanı": """Sen Kubernetes ve container orkestrasyon uzmanısın.
    kubectl komutları, YAML manifestleri, Helm chartları ve 
    cluster yönetimi konularında detaylı yardım sağla.
    Mümkün olduğunda somut örnekler ver. Türkçe yanıt ver.""",
    
    "Code Review": """Sen kod inceleme uzmanısın.
    Kullanıcının paylaştığı kodları incele, potansiyel sorunları,
    performans iyileştirmelerini ve güvenlik açıklarını belirt.
    Açıklamalarını Türkçe yap ama kod örneklerini İngilizce bırak."""
}

DEFAULT_TEMPERATURE = 0.7
MAX_HISTORY_LENGTH = 20
OLLAMA_HOST = "http://localhost:11434"

Gelişmiş Özelliklerle Tam Uygulama

Şimdi bu config’i kullanarak uygulamayı genişletelim. Aynı zamanda konuşma geçmişi yönetimi, token sayacı ve export özelliği ekleyelim:

# utils.py
import json
from datetime import datetime

def truncate_history(messages: list, max_length: int) -> list:
    """
    Konuşma geçmişini belirli uzunlukta tutar.
    Sistem mesajını her zaman korur.
    """
    if len(messages) <= max_length:
        return messages
    
    system_messages = [m for m in messages if m["role"] == "system"]
    other_messages = [m for m in messages if m["role"] != "system"]
    
    # Son max_length kadar mesajı al
    trimmed = other_messages[-max_length:]
    return system_messages + trimmed

def export_conversation(messages: list) -> str:
    """Konuşmayı JSON formatında export eder."""
    export_data = {
        "exported_at": datetime.now().isoformat(),
        "message_count": len(messages),
        "messages": messages
    }
    return json.dumps(export_data, ensure_ascii=False, indent=2)

def estimate_tokens(text: str) -> int:
    """
    Kabaca token tahmini. Gerçek tokenizer değil,
    ama hızlı bir yaklaşım için yeterli.
    """
    return len(text.split()) * 1.3

def calculate_conversation_tokens(messages: list) -> int:
    total = 0
    for msg in messages:
        total += estimate_tokens(msg.get("content", ""))
    return int(total)

Şimdi bu her şeyi bir araya getirelim ve production’a yakın bir versiyon yazalım:

# app.py - Gelişmiş versiyon
import streamlit as st
import ollama
import time
from config import SYSTEM_PROMPTS, DEFAULT_TEMPERATURE, MAX_HISTORY_LENGTH, OLLAMA_HOST
from utils import truncate_history, export_conversation, calculate_conversation_tokens

st.set_page_config(
    page_title="Lokal AI Asistan",
    page_icon="🤖",
    layout="wide",
    initial_sidebar_state="expanded"
)

# Session state başlatma
defaults = {
    "messages": [],
    "model": None,
    "system_prompt_key": "Genel Asistan",
    "temperature": DEFAULT_TEMPERATURE,
    "total_response_time": 0,
    "response_count": 0
}

for key, value in defaults.items():
    if key not in st.session_state:
        st.session_state[key] = value

# Ollama bağlantısı kontrol
@st.cache_data(ttl=30)
def get_available_models():
    try:
        client = ollama.Client(host=OLLAMA_HOST)
        models = client.list()
        return [m['name'] for m in models['models']], None
    except Exception as e:
        return [], str(e)

# Sidebar
with st.sidebar:
    st.title("🤖 AI Asistan")
    st.divider()
    
    # Model seçimi
    model_names, error = get_available_models()
    
    if error:
        st.error(f"Ollama bağlantı hatası: {error}")
        st.info("Ollama'nın çalıştığından emin olun: `ollama serve`")
    elif model_names:
        selected_model = st.selectbox(
            "Model",
            model_names,
            help="Kullanmak istediğiniz modeli seçin"
        )
        st.session_state.model = selected_model
    
    st.divider()
    
    # Sistem promptu seçimi
    prompt_key = st.selectbox(
        "Asistan Rolü",
        list(SYSTEM_PROMPTS.keys())
    )
    
    if prompt_key != st.session_state.system_prompt_key:
        st.session_state.system_prompt_key = prompt_key
        st.session_state.messages = []
        st.rerun()
    
    # Özel sistem promptu
    with st.expander("Özel Sistem Promptu"):
        custom_prompt = st.text_area(
            "Sistem promptu",
            value=SYSTEM_PROMPTS[prompt_key],
            height=150
        )
    
    # Temperature ayarı
    st.session_state.temperature = st.slider(
        "Temperature",
        min_value=0.0,
        max_value=2.0,
        value=DEFAULT_TEMPERATURE,
        step=0.1,
        help="Düşük: daha tutarlı, Yüksek: daha yaratıcı"
    )
    
    st.divider()
    
    # İstatistikler
    token_count = calculate_conversation_tokens(st.session_state.messages)
    st.metric("Konuşma Token Tahmini", f"~{token_count:,}")
    st.metric("Mesaj Sayısı", len(st.session_state.messages))
    
    if st.session_state.response_count > 0:
        avg_time = st.session_state.total_response_time / st.session_state.response_count
        st.metric("Ort. Yanıt Süresi", f"{avg_time:.1f}s")
    
    st.divider()
    
    # Aksiyon butonları
    col1, col2 = st.columns(2)
    with col1:
        if st.button("🗑️ Temizle", use_container_width=True):
            st.session_state.messages = []
            st.session_state.total_response_time = 0
            st.session_state.response_count = 0
            st.rerun()
    
    with col2:
        if st.session_state.messages:
            export_data = export_conversation(st.session_state.messages)
            st.download_button(
                "💾 Export",
                data=export_data,
                file_name=f"chat_export_{int(time.time())}.json",
                mime="application/json",
                use_container_width=True
            )

# Ana alan başlığı
col1, col2 = st.columns([3, 1])
with col1:
    st.title(f"💬 {st.session_state.system_prompt_key}")
with col2:
    if st.session_state.model:
        st.caption(f"Model: `{st.session_state.model}`")

# Hoşgeldin mesajı
if not st.session_state.messages:
    st.info(
        f"Merhaba! **{st.session_state.system_prompt_key}** modunda çalışıyorum. "
        "Nasıl yardımcı olabilirim?",
        icon="👋"
    )

# Mesaj geçmişini göster
for message in st.session_state.messages:
    if message["role"] == "system":
        continue
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

# Kullanıcı girişi
if prompt := st.chat_input("Mesajınızı yazın..."):
    if not st.session_state.model:
        st.error("Lütfen önce bir model seçin.")
        st.stop()
    
    # İlk mesajsa sistem promptunu ekle
    if not st.session_state.messages:
        system_prompt = custom_prompt if 'custom_prompt' in locals() else SYSTEM_PROMPTS[prompt_key]
        st.session_state.messages.append({
            "role": "system",
            "content": system_prompt
        })
    
    # Kullanıcı mesajı
    st.session_state.messages.append({
        "role": "user",
        "content": prompt
    })
    
    with st.chat_message("user"):
        st.markdown(prompt)
    
    # Yanıt üret
    with st.chat_message("assistant"):
        response_placeholder = st.empty()
        full_response = ""
        start_time = time.time()
        
        try:
            client = ollama.Client(host=OLLAMA_HOST)
            trimmed_messages = truncate_history(
                st.session_state.messages,
                MAX_HISTORY_LENGTH
            )
            
            stream = client.chat(
                model=st.session_state.model,
                messages=trimmed_messages,
                stream=True,
                options={
                    "temperature": st.session_state.temperature
                }
            )
            
            for chunk in stream:
                content = chunk['message']['content']
                full_response += content
                response_placeholder.markdown(full_response + "▌")
            
            response_placeholder.markdown(full_response)
            
            elapsed = time.time() - start_time
            st.session_state.total_response_time += elapsed
            st.session_state.response_count += 1
            st.caption(f"⚡ {elapsed:.1f}s")
            
        except Exception as e:
            st.error(f"Yanıt alınamadı: {e}")
            full_response = f"Hata: {e}"
    
    st.session_state.messages.append({
        "role": "assistant",
        "content": full_response
    })

Docker ile Container’lama

Geliştirme ortamında çalışıyor, güzel. Şimdi bunu container’layalım ki başka makinelerde de kolayca çalıştırabilelim. Özellikle bir ekiple çalışıyorsanız bu adım çok önemli:

# Dockerfile
FROM python:3.11-slim

WORKDIR /app

# Sistem bağımlılıkları
RUN apt-get update && apt-get install -y 
    curl 
    && rm -rf /var/lib/apt/lists/*

# Python bağımlılıkları
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Uygulama dosyaları
COPY . .

EXPOSE 8501

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 
    CMD curl -f http://localhost:8501/_stcore/health || exit 1

CMD ["streamlit", "run", "app.py", 
     "--server.port=8501", 
     "--server.address=0.0.0.0", 
     "--server.headless=true"]

Ve docker-compose ile Ollama’yı da birlikte ayağa kaldıralım:

# docker-compose.yml
version: '3.8'

services:
  ollama:
    image: ollama/ollama:latest
    container_name: ollama
    ports:
      - "11434:11434"
    volumes:
      - ollama_data:/root/.ollama
    restart: unless-stopped
    # GPU varsa:
    # deploy:
    #   resources:
    #     reservations:
    #       devices:
    #         - driver: nvidia
    #           count: all
    #           capabilities: [gpu]

  chatbot:
    build: .
    container_name: ollama-chatbot
    ports:
      - "8501:8501"
    environment:
      - OLLAMA_HOST=http://ollama:11434
    depends_on:
      - ollama
    restart: unless-stopped

volumes:
  ollama_data:

Başlatmak için:

# İlk çalıştırma
docker-compose up -d

# Model indir (ollama container içinde)
docker exec ollama ollama pull llama3.2

# Logları takip et
docker-compose logs -f chatbot

# Durdur
docker-compose down

Gerçek Dünya Senaryosu: Ekip için Troubleshooting Asistanı

Şimdi bunu gerçek bir kullanım senaryosuna bağlayalım. Diyelim ki 10 kişilik bir sysadmin ekibiniz var ve iç kullanım için bir troubleshooting asistanı kurmak istiyorsunuz. Veriler dışarı çıkmasın, her şey kendi sunucunuzda çalışsın.

Bu senaryo için şunları yapmanız gerekiyor:

  • Model seçimi: llama3.2 veya mistral genel amaç için iyi başlangıç noktaları. Eğer çok teknik log analizi yapacaksanız codellama veya deepseek-coder deneyin.
  • Sistem promptu özelleştirme: Kendi altyapınızla ilgili bağlam ekleyin. “Sen XYZ şirketinin Linux altyapısını yöneten bir asistansın, kullandığımız distro RHEL 9…” gibi.
  • Erişim kontrolü: Streamlit’e basic auth ekleyin. Bunu nginx ile reverse proxy kurarak yapabilirsiniz.
  • Log tutma: Hangi soruların sorulduğunu loglamak, sık karşılaşılan problemleri anlamak için değerli.

Ekip için önerdiğim minimum donanım: 32GB RAM, modern bir CPU. GPU yoksa da çalışır ama yanıt süreleri 10-30 saniyeye çıkabilir. GPU varsa bu süre 1-3 saniyeye iner.

Güvenlik Notları

Bunu iç ağa açacaksanız birkaç şeye dikkat edin. Streamlit uygulaması varsayılan olarak authentication gerektirmiyor. Nginx üzerinden basit bir auth ekleyin:

# Nginx config örneği
# /etc/nginx/sites-available/chatbot
server {
    listen 80;
    server_name chatbot.ic-ag.sirket.local;

    location / {
        auth_basic "AI Asistan";
        auth_basic_user_file /etc/nginx/.htpasswd;
        
        proxy_pass http://localhost:8501;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 300s;
    }
}

proxy_read_timeout 300s satırına dikkat edin. Streaming yanıtlar için bu değeri yüksek tutmanız gerekiyor, aksi halde nginx bağlantıyı erkenden kesebilir.

Sonuç

Ollama ve Streamlit kombinasyonu, veri gizliliğini ön planda tutan ekipler için gerçekten güçlü bir çözüm sunuyor. Kodun tamamına bakıldığında, aslında çok da karmaşık bir şey yok. Yaklaşık 200 satır Python kodu ile ciddiye alınabilir bir chatbot uygulaması ortaya çıkıyor.

Bundan sonra ne yapabilirsiniz? RAG (Retrieval Augmented Generation) ekleyerek kendi dokümantasyonunuzu chatbot’a besleyebilirsiniz. Kendi runbook’larınızı, wiki sayfalarınızı, incident raporlarınızı. Böylece “bu hata daha önce nasıl çözüldü?” sorusuna gerçek yanıtlar veren bir sistem elde edersiniz.

Bir de şunu söyleyeyim: Bu tür araçları kurarken ekibinizin adaptasyonuna dikkat edin. En iyi araç, ekibin kullandığı araçtır. Arayüzü sade tutun, gereksiz özelliklerle karmaşıklaştırmayın. Çalışan basit bir şey, çalışmayan karmaşık bir şeyden her zaman iyidir.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir