Skip to content

Chatbot #157

Open
Open

Description

@vinkkees

simple_chatbot.py

Een compacte 'AI' chatbot: TF-IDF retrieval + eenvoudige leerfunctie.

Scherp, voorspellend en makkelijk uit te breiden.

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
import readline # handig voor commandline historie op unix-achtige systemen

class SimpleChatBot:
def init(self, pairs=None):
"""
pairs: lijst van (user_tekst, bot_antwoord) tuples
De bot zoekt in de user_tekst voorbeelden naar de meest vergelijkbare zin
en geeft het corresponderende bot_antwoord terug.
"""
self.pairs = pairs or []
self._build_vectorizer()

def _build_vectorizer(self):
    # Corpus = alle user-zinnen in de dataset
    self.user_texts = [p[0] for p in self.pairs]
    if self.user_texts:
        self.vectorizer = TfidfVectorizer(ngram_range=(1,2), min_df=1)
        self.user_tfidf = self.vectorizer.fit_transform(self.user_texts)
    else:
        # lege vectorizer (voorkomt errors voor nog geen data)
        self.vectorizer = TfidfVectorizer(ngram_range=(1,2), min_df=1)
        self.user_tfidf = None

def respond(self, text, top_k=1):
    """
    Geef een antwoord op 'text'. Als er geen dataset is, antwoord simpele fallback.
    top_k bepaalt hoeveel beste matches worden overwogen voor randomisatie (optioneel).
    """
    text = text.strip()
    if not text:
        return "Zeg iets, of zwijg plechtig. (Maar zeg iets.)"

    # Als we nog geen voorbeelden hebben:
    if not self.user_texts:
        return "Ik ben nog leerling. Leer me iets door 'learn: jouw zin => mijn antwoord' te typen."

    # Transformeer en vergelijk
    x = self.vectorizer.transform([text])
    sims = cosine_similarity(x, self.user_tfidf).flatten()
    best_idx = np.argmax(sims)
    best_score = sims[best_idx]

    # Drempel: als geen goede match, geef fallback
    if best_score < 0.25:
        return ("Dat is intrigerend maar ik heb daar geen goede zet voor. "
                "Vertel me wat het betekent of leer me met: learn: vraag => antwoord")

    # soms wil je variatie: kies uit de top_k best overeenkomende antwoorden
    if top_k > 1:
        top_indices = np.argsort(sims)[-top_k:]
        chosen = np.random.choice(top_indices)
        return self.pairs[chosen][1]
    else:
        return self.pairs[best_idx][1]

def learn_from_pair(self, user_text, bot_answer):
    """Voeg een nieuw voorbeeld toe en rebuild de vectorizer."""
    self.pairs.append((user_text.strip(), bot_answer.strip()))
    self._build_vectorizer()

def save_to_file(self, path):
    """Sla de dataset op (eenvoudig formaat)."""
    with open(path, "w", encoding="utf-8") as f:
        for u, b in self.pairs:
            # tab-gescheiden, simpele escaping
            u_escaped = u.replace("\t", "    ").replace("\n", " ")
            b_escaped = b.replace("\t", "    ").replace("\n", " ")
            f.write(u_escaped + "\t" + b_escaped + "\n")

def load_from_file(self, path):
    """Laad dataset van disk."""
    new_pairs = []
    try:
        with open(path, "r", encoding="utf-8") as f:
            for line in f:
                if not line.strip(): continue
                parts = line.rstrip("\n").split("\t")
                if len(parts) >= 2:
                    new_pairs.append((parts[0], "\t".join(parts[1:])))
        self.pairs = new_pairs
        self._build_vectorizer()
    except FileNotFoundError:
        print(f"Geen dataset gevonden op {path}; start met lege dataset.")

def demo():
# Voorbeeld dataset: voeg hier je eigen moeilijke, schaakachtige zinnen toe.
initial_pairs = [
("hallo", "Salve. Wat wens je te weten?"),
("hoe gaat het", "Adequaat. En bij jou?"),
("wat is schaak", "Schaak is een strategisch bordspel waarin anticipatie essentieel is."),
("leer me schaken", "Begin met de basisprincipes: ontwikkel stukken, controleer centrum, en vermijd vroegtijdige koningszwakte."),
("vertel een mop", "Twee torens lopen naar het bord... de rest is vertelling."),
("bedankt", "Graag gedaan. Mijn circuits glimmen van voldoening.")
]

bot = SimpleChatBot(initial_pairs)
bot.load_from_file("my_bot_memory.txt")  # optioneel: laad eerdere leerdata

print("Slimme ChatBot (TF-IDF retrieval). Type 'quit' om te stoppen.")
print("Leer de bot nieuw gedrag met: learn: jouw vraag => mijn antwoord")
print("Sla geheugen op met: save")
print("Laad geheugen met: load")
print("-" * 60)

while True:
    try:
        user = input("Jij: ").strip()
    except (KeyboardInterrupt, EOFError):
        print("\nEinde. Memory wordt automatisch opgeslagen.")
        bot.save_to_file("my_bot_memory.txt")
        break

    if not user:
        continue

    if user.lower() in ("quit", "exit", "stop"):
        print("Bot: Tot ziens. Je zet was goed, je eindezelfde mind ook.")
        bot.save_to_file("my_bot_memory.txt")
        break

    # leer commando: learn: vraag => antwoord
    if user.lower().startswith("learn:"):
        payload = user[6:].strip()
        if "=>" in payload:
            q, a = payload.split("=>", 1)
            bot.learn_from_pair(q.strip(), a.strip())
            print("Bot: Acknowledged. Ik heb die zet opgeslagen.")
        else:
            print("Bot: Foutief leerformaat. Gebruik: learn: vraag => antwoord")
        continue

    if user.lower() == "save":
        bot.save_to_file("my_bot_memory.txt")
        print("Bot: geheugen opgeslagen naar my_bot_memory.txt")
        continue

    if user.lower() == "load":
        bot.load_from_file("my_bot_memory.txt")
        print("Bot: geheugen geladen (my_bot_memory.txt)")
        continue

    # normaal gesprek
    reply = bot.respond(user, top_k=3)
    print("Bot:", reply)

if name == "main":
demo()

Activity

  1. iamnotgoodatprogrammingplshelp commented on Nov 19, 2025

    @iamnotgoodatprogrammingplshelp

    Subject: Small Python syntax fixes to get your bot running

    This is a really solid concept for a lightweight chatbot! However, it currently won't run because of three specific syntax errors regarding Python's "magic methods" and indentation.

    Here is how to fix it:

    Fix the Constructor (The Double Underscores) Python classes require double underscores before and after init to recognize it as the setup function.

    Change: def init(self, pairs=None):

    To: def init(self, pairs=None):

  2. PatelNikhil666 commented on Feb 3, 2026

    @PatelNikhil666

    Clean separation of concerns (respond, learn_from_pair, persistence).
    Good fallback handling when confidence is low.
    top_k option adds response variation
    Safe handling of empty datasets.
    Rebuilding the vectorizer on every learn_from_pair is fine for small datasets, but may become slow at scale — consider batching or delayed rebuilds.
    Random selection from top_k could sometimes return weaker matches; you might want to restrict randomization to scores above a higher threshold.
    The similarity threshold (0.25) may need tuning as the dataset grows.

  3. iamnotgoodatprogrammingplshelp commented on Feb 3, 2026

    @iamnotgoodatprogrammingplshelp

    so what your problem is is that you have a problem with o(n^2) growth you can use an incremental learner or a Vector Database (like Chroma, Pinecone, or FAISS).

  4. Hardikrepo commented on Jul 1, 2026

    @Hardikrepo

    Following up on the O(n^2) growth point — here's a minimal-diff fix that avoids refitting the whole TF-IDF vectorizer on every learn_from_pair call, using scipy.sparse.vstack to append incrementally and a periodic rebuild to resync vocabulary:

    from scipy.sparse import vstack
    
    class SimpleChatBot:
        def __init__(self, pairs=None, rebuild_every=20):
            self.pairs = pairs or []
            self.rebuild_every = rebuild_every  # full refit cadence, to resync vocab/IDF
            self._pending_since_rebuild = 0
            self._build_vectorizer()
    
        def _build_vectorizer(self):
            self.user_texts = [p[0] for p in self.pairs]
            if self.user_texts:
                self.vectorizer = TfidfVectorizer(ngram_range=(1, 2), min_df=1)
                self.user_tfidf = self.vectorizer.fit_transform(self.user_texts)
            else:
                self.vectorizer = TfidfVectorizer(ngram_range=(1, 2), min_df=1)
                self.user_tfidf = None
            self._pending_since_rebuild = 0
    
        def learn_from_pair(self, user_text, bot_answer):
            """Add a new example without refitting TF-IDF on every call."""
            user_text, bot_answer = user_text.strip(), bot_answer.strip()
            self.pairs.append((user_text, bot_answer))
            self.user_texts.append(user_text)
    
            if self.user_tfidf is None:
                self._build_vectorizer()  # first example needs a real fit
                return
    
            # Reuse the already-fitted vocabulary/IDF weights instead of refitting.
            new_vec = self.vectorizer.transform([user_text])
            self.user_tfidf = vstack([self.user_tfidf, new_vec])
            self._pending_since_rebuild += 1
    
            # transform() drops n-grams outside the last-fitted vocabulary, so
            # periodically do a full rebuild to pick up new terms/IDF weights.
            if self._pending_since_rebuild >= self.rebuild_every:
                self._build_vectorizer()

    What changed and why:

    • learn_from_pair no longer calls _build_vectorizer() (a full fit_transform over the whole corpus) on every call — that's what made each call cost O(n) and a full session cost O(n²).
    • Instead it calls vectorizer.transform() (reuses existing vocab, no refit) and appends the new row with vstack — O(1) amortized per call.
    • Trade-off: between rebuilds, brand-new n-grams aren't yet in the vocabulary, so they're ignored in scoring for that pair until the next rebuild. rebuild_every (default 20) bounds how stale that gets — tune based on how bursty learning sessions are.
    • For larger scale, a vector DB (FAISS/Chroma, as mentioned above) is still the better long-term answer since it also avoids the linear cosine_similarity scan in respond() — this is just the smallest change that addresses the O(n²) issue directly.
  5. Hardikrepo commented on Jul 1, 2026

    @Hardikrepo

    Summary of the fix for the O(n^2) growth issue above:

    The root cause is that learn_from_pair calls _build_vectorizer(), which refits the TF-IDF vectorizer over the entire dataset on every single new example — so each call costs O(n), and a full learning session costs O(n²).

    The fix: instead of refitting on every call, reuse the already-fitted vectorizer's transform() to encode just the new example, and append it to the existing TF-IDF matrix with scipy.sparse.vstack — O(1) amortized per call. A full rebuild only runs periodically (e.g. every 20 additions) to resync the vocabulary/IDF weights with any new terms, bounding the staleness trade-off.

    Full code for the change is in my earlier comment above. For larger-scale use, a vector DB (FAISS/Chroma) is still the better long-term path since it also avoids the linear cosine_similarity scan in respond() — this fix targets just the specific O(n²) concern with a minimal change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions