bot
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitignore +12 -0
- agents/__init__.py +0 -0
- agents/comparison.py +47 -0
- agents/followup.py +68 -0
- agents/intent_classifier.py +208 -0
- agents/router.py +173 -0
- agents/topic_categories.py +56 -0
- app.py +94 -0
- auth/__init__.py +0 -0
- auth/db.py +22 -0
- auth/models.py +41 -0
- auth/routes.py +87 -0
- chat_routes.py +82 -0
- chat_service.py +126 -0
- config.py +90 -0
- database/__init__.py +0 -0
- database/app.db +0 -0
- evaluation/__init__.py +0 -0
- evaluation/evaluate.py +222 -0
- evaluation/last_report.json +302 -0
- evaluation/questions.json +124 -0
- history/__init__.py +0 -0
- history/models.py +71 -0
- history/service.py +91 -0
- history/session_manager.py +46 -0
- llm/__init__.py +0 -0
- llm/generate.py +64 -0
- llm/groq_provider.py +82 -0
- llm/local_provider.py +98 -0
- llm/prompts.py +203 -0
- llm/provider.py +33 -0
- logs/__init__.py +0 -0
- logs/app.log +440 -0
- logs/logger.py +48 -0
- memory/__init__.py +0 -0
- memory/mem0_client.py +137 -0
- rag/__init__.py +0 -0
- rag/build_index.py +211 -0
- rag/generate.py +147 -0
- rag/knowledge_base/01_knapsack.txt +37 -0
- rag/knowledge_base/activity_selection_problem.txt +41 -0
- rag/knowledge_base/avl_tree.txt +63 -0
- rag/knowledge_base/backtracking_overview.txt +42 -0
- rag/knowledge_base/bellman-ford_algorithm.txt +49 -0
- rag/knowledge_base/bfs.txt +29 -0
- rag/knowledge_base/binary_search.txt +25 -0
- rag/knowledge_base/binary_search_tree.txt +35 -0
- rag/knowledge_base/binary_tree.txt +43 -0
- rag/knowledge_base/bit_manipulation_basics.txt +24 -0
- rag/knowledge_base/bubble_sort.txt +33 -0
.gitignore
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ChromaDB
|
| 2 |
+
rag/chroma_db/
|
| 3 |
+
memory/mem0_store/chroma_db/
|
| 4 |
+
|
| 5 |
+
# SQLite
|
| 6 |
+
*.sqlite3
|
| 7 |
+
*.bin
|
| 8 |
+
|
| 9 |
+
# Python
|
| 10 |
+
__pycache__/
|
| 11 |
+
.venv/
|
| 12 |
+
.env
|
agents/__init__.py
ADDED
|
File without changes
|
agents/comparison.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Comparison agent.
|
| 3 |
+
|
| 4 |
+
Handles queries like "difference between BFS and DFS" — retrieves chunks
|
| 5 |
+
per-topic (rather than pooled) so the prompt builder can present each
|
| 6 |
+
topic's relevant sections side by side.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from logs.logger import get_logger
|
| 10 |
+
|
| 11 |
+
logger = get_logger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def gather_comparison_context(topics: list, retriever, top_k_per_topic: int = None) -> dict:
|
| 15 |
+
"""
|
| 16 |
+
Args:
|
| 17 |
+
topics: canonical topic names, e.g. ["Breadth-First Search", "Depth-First Search"].
|
| 18 |
+
retriever: a rag.retriever.Retriever instance.
|
| 19 |
+
top_k_per_topic: chunks to retrieve per topic (defaults to retriever/config default).
|
| 20 |
+
|
| 21 |
+
Returns:
|
| 22 |
+
{
|
| 23 |
+
"Breadth-First Search": [chunk, chunk, ...],
|
| 24 |
+
"Depth-First Search": [chunk, chunk, ...],
|
| 25 |
+
}
|
| 26 |
+
A topic with no chunks clearing the similarity threshold maps to [].
|
| 27 |
+
"""
|
| 28 |
+
context = retriever.retrieve_topics(topics, top_k_per_topic=top_k_per_topic)
|
| 29 |
+
|
| 30 |
+
for topic, chunks in context.items():
|
| 31 |
+
if not chunks:
|
| 32 |
+
logger.warning(
|
| 33 |
+
"Comparison agent: no chunks passed the similarity threshold for topic %r",
|
| 34 |
+
topic,
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
return context
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def has_sufficient_context(comparison_context: dict) -> bool:
|
| 41 |
+
"""
|
| 42 |
+
True only if every topic in the comparison has at least one retrieved
|
| 43 |
+
chunk. If any topic comes back empty, the caller should fall back to
|
| 44 |
+
the "insufficient information" response rather than comparing a topic
|
| 45 |
+
it knows nothing about against one it does.
|
| 46 |
+
"""
|
| 47 |
+
return all(len(chunks) > 0 for chunks in comparison_context.values())
|
agents/followup.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Follow-up agent.
|
| 3 |
+
|
| 4 |
+
Handles queries like "can you explain that again?" or "what about the
|
| 5 |
+
space complexity?" that only make sense in light of recent conversation.
|
| 6 |
+
|
| 7 |
+
Strategy: reuse recent conversation history as context by default, and
|
| 8 |
+
only fall back to a fresh embedding-based retrieval if the follow-up
|
| 9 |
+
mentions a topic that wasn't part of the recent conversation (e.g. user
|
| 10 |
+
pivots to a new topic with a pronoun-only phrasing).
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from agents.intent_classifier import extract_topics,topics_in_recent_history
|
| 14 |
+
from logs.logger import get_logger
|
| 15 |
+
|
| 16 |
+
logger = get_logger(__name__)
|
| 17 |
+
|
| 18 |
+
def gather_followup_context(
|
| 19 |
+
query: str,
|
| 20 |
+
recent_messages: list,
|
| 21 |
+
retriever,
|
| 22 |
+
top_k: int = None,
|
| 23 |
+
) -> dict:
|
| 24 |
+
"""
|
| 25 |
+
Args:
|
| 26 |
+
query: the current follow-up question.
|
| 27 |
+
recent_messages: recent conversation turns, oldest -> newest
|
| 28 |
+
(see history/service.py for how these are fetched;
|
| 29 |
+
bounded by config.MAX_RECENT_HISTORY_MESSAGES).
|
| 30 |
+
retriever: a rag.retriever.Retriever instance.
|
| 31 |
+
top_k: chunks to retrieve if a fresh lookup is needed.
|
| 32 |
+
|
| 33 |
+
Returns:
|
| 34 |
+
{
|
| 35 |
+
"reused_topics": [...], # topics inferred from history
|
| 36 |
+
"additional_chunks": [...], # only populated if a new topic
|
| 37 |
+
# was mentioned in `query` itself
|
| 38 |
+
"recent_messages": recent_messages,
|
| 39 |
+
}
|
| 40 |
+
"""
|
| 41 |
+
reused_topics = topics_in_recent_history(recent_messages)
|
| 42 |
+
|
| 43 |
+
# Does the follow-up query itself introduce a topic not seen recently?
|
| 44 |
+
query_topics = extract_topics(query)
|
| 45 |
+
new_topics = [t for t in query_topics if t not in reused_topics]
|
| 46 |
+
|
| 47 |
+
additional_chunks = []
|
| 48 |
+
if new_topics:
|
| 49 |
+
logger.info(
|
| 50 |
+
"Follow-up query introduces new topic(s) not in recent history: %s",
|
| 51 |
+
new_topics,
|
| 52 |
+
)
|
| 53 |
+
for topic in new_topics:
|
| 54 |
+
additional_chunks.extend(
|
| 55 |
+
retriever.retrieve(query=query, top_k=top_k, topic_filter=[topic])
|
| 56 |
+
)
|
| 57 |
+
else:
|
| 58 |
+
logger.info(
|
| 59 |
+
"Follow-up query reuses existing conversation context (topics=%s); "
|
| 60 |
+
"skipping fresh retrieval.",
|
| 61 |
+
reused_topics,
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
return {
|
| 65 |
+
"reused_topics": reused_topics,
|
| 66 |
+
"additional_chunks": additional_chunks,
|
| 67 |
+
"recent_messages": recent_messages,
|
| 68 |
+
}
|
agents/intent_classifier.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Intent classification helpers used by the agent router.
|
| 3 |
+
|
| 4 |
+
This module is intentionally rule-based (regex + fuzzy topic-name matching)
|
| 5 |
+
rather than LLM-based, since the LLM provider abstraction (llm/) hasn't
|
| 6 |
+
been wired in yet. It's isolated behind plain functions so it can be
|
| 7 |
+
swapped for an LLM-based classifier later without changing router.py's
|
| 8 |
+
interface — router.py only calls extract_topics(), is_comparison_query(),
|
| 9 |
+
and is_followup_query().
|
| 10 |
+
|
| 11 |
+
No keyword-based *retrieval* happens here — this only decides ROUTING.
|
| 12 |
+
Actual knowledge retrieval is always embedding-based (see rag/retriever.py).
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import difflib
|
| 16 |
+
import json
|
| 17 |
+
import re
|
| 18 |
+
|
| 19 |
+
import config
|
| 20 |
+
|
| 21 |
+
# ---------------------------------------------------------------------------
|
| 22 |
+
# Topic vocabulary (loaded once from topic_metadata.json)
|
| 23 |
+
# ---------------------------------------------------------------------------
|
| 24 |
+
|
| 25 |
+
_FUZZY_MATCH_CUTOFF = 0.85
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _load_known_topics() -> list:
|
| 29 |
+
"""Returns the list of canonical topic names, e.g. ['Binary Search', ...]."""
|
| 30 |
+
with open(config.TOPIC_METADATA_PATH, "r", encoding="utf-8") as f:
|
| 31 |
+
metadata = json.load(f)
|
| 32 |
+
return sorted({entry["topic"] for entry in metadata.values() if entry.get("topic")})
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
_KNOWN_TOPICS = None
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def get_known_topics() -> list:
|
| 39 |
+
global _KNOWN_TOPICS
|
| 40 |
+
if _KNOWN_TOPICS is None:
|
| 41 |
+
_KNOWN_TOPICS = _load_known_topics()
|
| 42 |
+
return _KNOWN_TOPICS
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def refresh_known_topics() -> None:
|
| 46 |
+
"""Call after topic_metadata.json changes (e.g. after a re-index) to
|
| 47 |
+
force reloading the topic vocabulary on next use."""
|
| 48 |
+
global _KNOWN_TOPICS
|
| 49 |
+
_KNOWN_TOPICS = None
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
# ---------------------------------------------------------------------------
|
| 53 |
+
# Comparison detection
|
| 54 |
+
# ---------------------------------------------------------------------------
|
| 55 |
+
|
| 56 |
+
_COMPARISON_PATTERNS = [
|
| 57 |
+
re.compile(r"\bdifference between\b", re.IGNORECASE),
|
| 58 |
+
re.compile(r"\bvs\.?\b", re.IGNORECASE),
|
| 59 |
+
re.compile(r"\bversus\b", re.IGNORECASE),
|
| 60 |
+
re.compile(r"\bcompare(d|s)?\b", re.IGNORECASE),
|
| 61 |
+
re.compile(r"\bwhich (one )?is (better|faster|more efficient)\b", re.IGNORECASE),
|
| 62 |
+
re.compile(r"\bpros and cons of\b", re.IGNORECASE),
|
| 63 |
+
]
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def is_comparison_query(query: str) -> bool:
|
| 67 |
+
return any(p.search(query) for p in _COMPARISON_PATTERNS)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# ---------------------------------------------------------------------------
|
| 71 |
+
# Follow-up detection
|
| 72 |
+
# ---------------------------------------------------------------------------
|
| 73 |
+
|
| 74 |
+
_FOLLOWUP_PATTERNS = [
|
| 75 |
+
re.compile(r"^\s*(explain|say|show)?\s*(that|this|it)\b.*again\b", re.IGNORECASE),
|
| 76 |
+
re.compile(r"^\s*what (do you mean|about (that|it|this))\b", re.IGNORECASE),
|
| 77 |
+
re.compile(r"^\s*(can you|could you)\s+(clarify|elaborate|expand)\b", re.IGNORECASE),
|
| 78 |
+
re.compile(r"^\s*(and|so)\b", re.IGNORECASE),
|
| 79 |
+
re.compile(r"^\s*(why|how) (is that|does that work)\b", re.IGNORECASE),
|
| 80 |
+
re.compile(r"\b(more|another) example\b", re.IGNORECASE),
|
| 81 |
+
re.compile(r"^\s*(simpler|simplify|dumb it down|eli5)\b", re.IGNORECASE),
|
| 82 |
+
]
|
| 83 |
+
|
| 84 |
+
# Bare pronoun references with no topic name mentioned, e.g. "explain it",
|
| 85 |
+
# "why is that", "tell me more"
|
| 86 |
+
_PRONOUN_ONLY_PATTERN = re.compile(
|
| 87 |
+
r"\b(it|that|this|those|these)\b", re.IGNORECASE
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
_WORKED_EXAMPLE_PATTERN = re.compile(
|
| 91 |
+
r"\d+(?:\s*,\s*\d+){1,}.*\b(this|that|same)\b.*\b(search|sort|algorithm|method|approach|traversal)\b",
|
| 92 |
+
re.IGNORECASE,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
def is_followup_query(query: str, has_conversation_history: bool) -> bool:
|
| 96 |
+
"""
|
| 97 |
+
A query is treated as a follow-up when:
|
| 98 |
+
1. There IS prior conversation to follow up on, AND
|
| 99 |
+
2. Either it matches a known follow-up phrasing pattern, OR
|
| 100 |
+
it's short, contains a bare pronoun reference, and mentions no
|
| 101 |
+
known topic by name (i.e. it can't stand on its own).
|
| 102 |
+
"""
|
| 103 |
+
if not has_conversation_history:
|
| 104 |
+
return False
|
| 105 |
+
|
| 106 |
+
if any(p.search(query) for p in _FOLLOWUP_PATTERNS):
|
| 107 |
+
return True
|
| 108 |
+
|
| 109 |
+
if _WORKED_EXAMPLE_PATTERN.search(query):
|
| 110 |
+
return True
|
| 111 |
+
|
| 112 |
+
word_count = len(query.strip().split())
|
| 113 |
+
has_pronoun = bool(_PRONOUN_ONLY_PATTERN.search(query))
|
| 114 |
+
mentions_topic = bool(extract_topics(query))
|
| 115 |
+
|
| 116 |
+
if has_pronoun and not mentions_topic and word_count <= 8:
|
| 117 |
+
return True
|
| 118 |
+
|
| 119 |
+
return False
|
| 120 |
+
|
| 121 |
+
_GENERIC_COMPARISON_PATTERN = re.compile(
|
| 122 |
+
r"\b(compare|comparison|vs\.?|versus|difference|different from|how does .* (differ|compare))\b.*"
|
| 123 |
+
r"\b(other|similar|related|different|alternative)\b",
|
| 124 |
+
re.IGNORECASE,
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def is_generic_comparison_followup(query: str) -> bool:
|
| 129 |
+
"""
|
| 130 |
+
Matches comparison phrasing that references *other* algorithms without
|
| 131 |
+
naming a second one explicitly, e.g. "compare with other graph
|
| 132 |
+
algorithms", "how does this differ from similar approaches".
|
| 133 |
+
"""
|
| 134 |
+
return bool(_GENERIC_COMPARISON_PATTERN.search(query))
|
| 135 |
+
|
| 136 |
+
def topics_in_recent_history(recent_messages: list) -> list:
|
| 137 |
+
"""
|
| 138 |
+
Scans recent conversation turns (both user and assistant messages) for
|
| 139 |
+
known topic mentions. Order preserved, most-recently-mentioned last.
|
| 140 |
+
"""
|
| 141 |
+
topics = []
|
| 142 |
+
for message in recent_messages:
|
| 143 |
+
for topic in extract_topics(message.get("content", "")):
|
| 144 |
+
if topic not in topics:
|
| 145 |
+
topics.append(topic)
|
| 146 |
+
else:
|
| 147 |
+
topics.remove(topic)
|
| 148 |
+
topics.append(topic) # bump to most-recent position
|
| 149 |
+
return topics
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
# ---------------------------------------------------------------------------
|
| 153 |
+
# Topic extraction (fuzzy match against known topic names)
|
| 154 |
+
# ---------------------------------------------------------------------------
|
| 155 |
+
|
| 156 |
+
def extract_topics(query: str) -> list:
|
| 157 |
+
"""
|
| 158 |
+
Returns the list of known topic names mentioned in the query, matched
|
| 159 |
+
via substring + fuzzy matching (handles typos/casing/partial names like
|
| 160 |
+
"binary search" or "BFS traversal").
|
| 161 |
+
|
| 162 |
+
This is NOT used for retrieval (retrieval stays purely embedding-based)
|
| 163 |
+
— it's only used for ROUTING decisions: deciding single-topic vs.
|
| 164 |
+
comparison, and picking which topics a comparison should retrieve.
|
| 165 |
+
"""
|
| 166 |
+
query_lower = query.lower()
|
| 167 |
+
found = []
|
| 168 |
+
|
| 169 |
+
for topic in get_known_topics():
|
| 170 |
+
topic_lower = topic.lower()
|
| 171 |
+
# Strip a trailing parenthetical, e.g. "Kruskal's Algorithm (MST)"
|
| 172 |
+
# -> "kruskal's algorithm", since people ask using the plain name,
|
| 173 |
+
# not the parenthetical qualifier.
|
| 174 |
+
topic_core = re.sub(r"\s*\([^)]*\)\s*$", "", topic_lower).strip()
|
| 175 |
+
|
| 176 |
+
# Direct substring match (full name or core name without qualifier)
|
| 177 |
+
if topic_lower in query_lower or (topic_core and topic_core in query_lower):
|
| 178 |
+
found.append(topic)
|
| 179 |
+
continue
|
| 180 |
+
|
| 181 |
+
# Acronym match, e.g. "Depth-First Search" -> "DFS", "Breadth-First
|
| 182 |
+
# Search" -> "BFS". Common in how people actually ask DSA questions.
|
| 183 |
+
acronym = "".join(w[0] for w in re.split(r"[\s\-]+", topic) if w).lower()
|
| 184 |
+
if len(acronym) >= 3 and re.search(rf"\b{re.escape(acronym)}\b", query_lower):
|
| 185 |
+
found.append(topic)
|
| 186 |
+
continue
|
| 187 |
+
|
| 188 |
+
# Fuzzy match against each sliding n-gram window of the query,
|
| 189 |
+
# sized to the topic's word count, to catch typos/near-matches.
|
| 190 |
+
topic_words = topic_lower.split()
|
| 191 |
+
query_words = query_lower.split()
|
| 192 |
+
window = len(topic_words)
|
| 193 |
+
for i in range(len(query_words) - window + 1):
|
| 194 |
+
candidate = " ".join(query_words[i : i + window])
|
| 195 |
+
ratio = difflib.SequenceMatcher(None, candidate, topic_lower).ratio()
|
| 196 |
+
if ratio >= _FUZZY_MATCH_CUTOFF:
|
| 197 |
+
found.append(topic)
|
| 198 |
+
break
|
| 199 |
+
|
| 200 |
+
# De-duplicate while preserving order
|
| 201 |
+
seen = set()
|
| 202 |
+
deduped = []
|
| 203 |
+
for t in found:
|
| 204 |
+
if t not in seen:
|
| 205 |
+
seen.add(t)
|
| 206 |
+
deduped.append(t)
|
| 207 |
+
return deduped
|
| 208 |
+
|
agents/router.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Agent router.
|
| 3 |
+
|
| 4 |
+
Decides how each incoming query should be processed and gathers the
|
| 5 |
+
context needed for the corresponding prompt builder (llm/prompts.py,
|
| 6 |
+
built in a later layer). This module does NOT call the LLM — it only
|
| 7 |
+
classifies + retrieves.
|
| 8 |
+
|
| 9 |
+
Routes:
|
| 10 |
+
SINGLE_TOPIC - e.g. "Explain Merge Sort" -> retrieve one topic
|
| 11 |
+
COMPARISON - e.g. "Difference between BFS/DFS" -> retrieve multiple topics
|
| 12 |
+
FOLLOWUP - e.g. "Can you explain that again?" -> reuse conversation context
|
| 13 |
+
OUT_OF_SCOPE - e.g. "Who won the World Cup?" -> reject politely
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from dataclasses import dataclass, field
|
| 17 |
+
from enum import Enum
|
| 18 |
+
|
| 19 |
+
from agents.comparison import gather_comparison_context, has_sufficient_context
|
| 20 |
+
from agents.followup import gather_followup_context
|
| 21 |
+
from agents.intent_classifier import (
|
| 22 |
+
extract_topics,
|
| 23 |
+
is_comparison_query,
|
| 24 |
+
is_followup_query,
|
| 25 |
+
is_generic_comparison_followup,
|
| 26 |
+
topics_in_recent_history,
|
| 27 |
+
)
|
| 28 |
+
from agents.topic_categories import get_sibling_topics
|
| 29 |
+
from logs.logger import get_logger
|
| 30 |
+
|
| 31 |
+
logger = get_logger(__name__)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class RouteType(str, Enum):
|
| 35 |
+
SINGLE_TOPIC = "single_topic"
|
| 36 |
+
COMPARISON = "comparison"
|
| 37 |
+
FOLLOWUP = "followup"
|
| 38 |
+
OUT_OF_SCOPE = "out_of_scope"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@dataclass
|
| 42 |
+
class RouteDecision:
|
| 43 |
+
route_type: RouteType
|
| 44 |
+
query: str
|
| 45 |
+
topics: list = field(default_factory=list)
|
| 46 |
+
# Populated by gather_context() below, per route type:
|
| 47 |
+
single_chunks: list = field(default_factory=list)
|
| 48 |
+
comparison_context: dict = field(default_factory=dict)
|
| 49 |
+
followup_context: dict = field(default_factory=dict)
|
| 50 |
+
|
| 51 |
+
def to_log_dict(self) -> dict:
|
| 52 |
+
return {
|
| 53 |
+
"route_type": self.route_type.value,
|
| 54 |
+
"query": self.query,
|
| 55 |
+
"topics": self.topics,
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def classify(query: str, has_conversation_history: bool = False, recent_topics: list = None) -> RouteDecision:
|
| 60 |
+
"""
|
| 61 |
+
Pure classification step (no retrieval yet). Order matters:
|
| 62 |
+
1. Follow-up check first — a short pronoun-based query with existing
|
| 63 |
+
history should be treated as a follow-up even if it superficially
|
| 64 |
+
contains comparison words.
|
| 65 |
+
2. Comparison check — requires comparison phrasing AND 2+ topics.
|
| 66 |
+
3. Single topic — exactly one recognized topic mentioned.
|
| 67 |
+
4. Otherwise — out of scope.
|
| 68 |
+
"""
|
| 69 |
+
recent_topics = recent_topics or []
|
| 70 |
+
|
| 71 |
+
if is_followup_query(query, has_conversation_history):
|
| 72 |
+
decision = RouteDecision(route_type=RouteType.FOLLOWUP, query=query)
|
| 73 |
+
logger.info("Routed to FOLLOWUP: %r", query)
|
| 74 |
+
return decision
|
| 75 |
+
|
| 76 |
+
topics = extract_topics(query)
|
| 77 |
+
|
| 78 |
+
if is_comparison_query(query) and len(topics) >= 2:
|
| 79 |
+
decision = RouteDecision(route_type=RouteType.COMPARISON, query=query, topics=topics)
|
| 80 |
+
logger.info("Routed to COMPARISON: %r | topics=%s", query, topics)
|
| 81 |
+
return decision
|
| 82 |
+
|
| 83 |
+
# NEW: comparison referencing the previous topic implicitly, e.g.
|
| 84 |
+
# "compare with other graph algorithms" after discussing BFS.
|
| 85 |
+
if is_generic_comparison_followup(query) and recent_topics:
|
| 86 |
+
anchor_topic = recent_topics[-1]
|
| 87 |
+
siblings = get_sibling_topics(anchor_topic, exclude=topics, limit=2)
|
| 88 |
+
combined = list(dict.fromkeys(topics + [anchor_topic] + siblings))
|
| 89 |
+
if len(combined) >= 2:
|
| 90 |
+
decision = RouteDecision(route_type=RouteType.COMPARISON, query=query, topics=combined)
|
| 91 |
+
logger.info(
|
| 92 |
+
"Routed to COMPARISON (category follow-up): %r | anchor=%s | topics=%s",
|
| 93 |
+
query, anchor_topic, combined,
|
| 94 |
+
)
|
| 95 |
+
return decision
|
| 96 |
+
|
| 97 |
+
if len(topics) >= 1:
|
| 98 |
+
decision = RouteDecision(route_type=RouteType.SINGLE_TOPIC, query=query, topics=topics[:1])
|
| 99 |
+
logger.info("Routed to SINGLE_TOPIC: %r | topic=%s", query, topics[0])
|
| 100 |
+
return decision
|
| 101 |
+
|
| 102 |
+
decision = RouteDecision(route_type=RouteType.OUT_OF_SCOPE, query=query)
|
| 103 |
+
logger.info("Routed to OUT_OF_SCOPE: %r", query)
|
| 104 |
+
return decision
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def gather_context(
|
| 108 |
+
decision: RouteDecision,
|
| 109 |
+
retriever,
|
| 110 |
+
recent_messages: list = None,
|
| 111 |
+
) -> RouteDecision:
|
| 112 |
+
"""
|
| 113 |
+
Populates the appropriate context field on `decision` based on its
|
| 114 |
+
route_type. Mutates and returns the same decision object.
|
| 115 |
+
|
| 116 |
+
Args:
|
| 117 |
+
decision: output of classify().
|
| 118 |
+
retriever: a rag.retriever.Retriever instance.
|
| 119 |
+
recent_messages: required for FOLLOWUP routes — recent conversation
|
| 120 |
+
turns, oldest -> newest.
|
| 121 |
+
"""
|
| 122 |
+
recent_messages = recent_messages or []
|
| 123 |
+
|
| 124 |
+
if decision.route_type == RouteType.SINGLE_TOPIC:
|
| 125 |
+
topic = decision.topics[0] if decision.topics else None
|
| 126 |
+
decision.single_chunks = retriever.retrieve(
|
| 127 |
+
query=decision.query,
|
| 128 |
+
topic_filter=[topic] if topic else None,
|
| 129 |
+
bypass_threshold_if_single_topic=True,
|
| 130 |
+
)
|
| 131 |
+
if not decision.single_chunks:
|
| 132 |
+
logger.warning(
|
| 133 |
+
"SINGLE_TOPIC query had no chunks pass the similarity "
|
| 134 |
+
"threshold: %r",
|
| 135 |
+
decision.query,
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
elif decision.route_type == RouteType.COMPARISON:
|
| 139 |
+
decision.comparison_context = gather_comparison_context(decision.topics, retriever)
|
| 140 |
+
if not has_sufficient_context(decision.comparison_context):
|
| 141 |
+
logger.warning(
|
| 142 |
+
"COMPARISON query missing sufficient context for one or "
|
| 143 |
+
"more topics: %s",
|
| 144 |
+
decision.topics,
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
elif decision.route_type == RouteType.FOLLOWUP:
|
| 148 |
+
decision.followup_context = gather_followup_context(
|
| 149 |
+
query=decision.query,
|
| 150 |
+
recent_messages=recent_messages,
|
| 151 |
+
retriever=retriever,
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
# OUT_OF_SCOPE needs no retrieval.
|
| 155 |
+
|
| 156 |
+
return decision
|
| 157 |
+
|
| 158 |
+
def route(query: str, retriever, recent_messages: list = None) -> RouteDecision:
|
| 159 |
+
"""
|
| 160 |
+
Convenience entrypoint combining classify() + gather_context().
|
| 161 |
+
|
| 162 |
+
recent_messages: recent conversation turns (oldest -> newest). Presence
|
| 163 |
+
of any messages here signals "has_conversation_history=True" for
|
| 164 |
+
follow-up detection.
|
| 165 |
+
"""
|
| 166 |
+
recent_messages = recent_messages or []
|
| 167 |
+
recent_topics = topics_in_recent_history(recent_messages)
|
| 168 |
+
decision = classify(
|
| 169 |
+
query,
|
| 170 |
+
has_conversation_history=len(recent_messages) > 0,
|
| 171 |
+
recent_topics=recent_topics,
|
| 172 |
+
)
|
| 173 |
+
return gather_context(decision, retriever, recent_messages=recent_messages)
|
agents/topic_categories.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Category lookups for topic_metadata.json.
|
| 3 |
+
|
| 4 |
+
Used by the router to resolve "compare this with other X" style queries
|
| 5 |
+
that reference the previous topic implicitly, without naming a second
|
| 6 |
+
topic explicitly (e.g. "compare BFS with other graph algorithms").
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import json
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
import config
|
| 13 |
+
from logs.logger import get_logger
|
| 14 |
+
|
| 15 |
+
logger = get_logger(__name__)
|
| 16 |
+
|
| 17 |
+
_metadata_cache = None
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _load_metadata() -> dict:
|
| 21 |
+
global _metadata_cache
|
| 22 |
+
if _metadata_cache is None:
|
| 23 |
+
if not os.path.exists(config.TOPIC_METADATA_PATH):
|
| 24 |
+
logger.warning("topic_metadata.json not found; category lookups disabled.")
|
| 25 |
+
_metadata_cache = {}
|
| 26 |
+
else:
|
| 27 |
+
with open(config.TOPIC_METADATA_PATH, "r", encoding="utf-8") as f:
|
| 28 |
+
_metadata_cache = json.load(f)
|
| 29 |
+
return _metadata_cache
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def get_category(topic_name: str) -> str | None:
|
| 33 |
+
"""Returns the category string for a canonical topic name, or None."""
|
| 34 |
+
for entry in _load_metadata().values():
|
| 35 |
+
if entry.get("topic") == topic_name:
|
| 36 |
+
return entry.get("category")
|
| 37 |
+
return None
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def get_sibling_topics(topic_name: str, exclude: list = None, limit: int = 2) -> list:
|
| 41 |
+
"""
|
| 42 |
+
Returns up to `limit` other topic names sharing the same category as
|
| 43 |
+
`topic_name` (e.g. siblings of "Breadth-First Search" in category
|
| 44 |
+
"Graph Traversal" -> ["Depth-First Search", "Dijkstra's Algorithm"]).
|
| 45 |
+
"""
|
| 46 |
+
exclude = set(exclude or []) | {topic_name}
|
| 47 |
+
category = get_category(topic_name)
|
| 48 |
+
if not category:
|
| 49 |
+
return []
|
| 50 |
+
|
| 51 |
+
siblings = [
|
| 52 |
+
entry["topic"]
|
| 53 |
+
for entry in _load_metadata().values()
|
| 54 |
+
if entry.get("category") == category and entry.get("topic") not in exclude
|
| 55 |
+
]
|
| 56 |
+
return siblings[:limit]
|
app.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Flask application entrypoint.
|
| 3 |
+
|
| 4 |
+
At this stage of the build (Layer 1), only the database and authentication
|
| 5 |
+
are wired up so this layer can be tested in isolation. Chat/RAG/agent
|
| 6 |
+
routes will be added in later layers.
|
| 7 |
+
"""
|
| 8 |
+
# http://127.0.0.1:7860/
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
import threading
|
| 13 |
+
|
| 14 |
+
# Some corporate proxies / VPNs set HTTP_PROXY / HTTPS_PROXY, which makes
|
| 15 |
+
# Gradio's internal "is localhost reachable" check fail even for 127.0.0.1.
|
| 16 |
+
os.environ["NO_PROXY"] = "127.0.0.1,localhost," + os.environ.get("NO_PROXY", "")
|
| 17 |
+
os.environ["no_proxy"] = os.environ["NO_PROXY"]
|
| 18 |
+
|
| 19 |
+
# --- Workaround for a gradio_client bug (gradio 4.44.1 + newer pydantic) ---
|
| 20 |
+
# When a schema (or a nested value like `additionalProperties`) is a plain
|
| 21 |
+
# bool (True/False) rather than a dict, gradio_client's schema walker either
|
| 22 |
+
# crashes with "TypeError: argument of type 'bool' is not iterable" or raises
|
| 23 |
+
# "APIInfoParseError: Cannot parse schema True". This happens while Gradio
|
| 24 |
+
# builds the page's embedded API info, unconditionally, on every request to
|
| 25 |
+
# "/" - so it can't be avoided via show_api=False. Patch both entry points.
|
| 26 |
+
import gradio_client.utils as _gc_utils
|
| 27 |
+
|
| 28 |
+
_orig_get_type = _gc_utils.get_type
|
| 29 |
+
_orig_json_schema_to_python_type = _gc_utils._json_schema_to_python_type
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _safe_get_type(schema):
|
| 33 |
+
if isinstance(schema, bool):
|
| 34 |
+
return "Any"
|
| 35 |
+
return _orig_get_type(schema)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _safe_json_schema_to_python_type(schema, defs=None):
|
| 39 |
+
if isinstance(schema, bool):
|
| 40 |
+
return "Any"
|
| 41 |
+
return _orig_json_schema_to_python_type(schema, defs)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
_gc_utils.get_type = _safe_get_type
|
| 45 |
+
_gc_utils._json_schema_to_python_type = _safe_json_schema_to_python_type
|
| 46 |
+
# --- end workaround ---
|
| 47 |
+
|
| 48 |
+
from flask import Flask
|
| 49 |
+
|
| 50 |
+
import config
|
| 51 |
+
from auth.db import init_db
|
| 52 |
+
from auth.routes import auth_bp
|
| 53 |
+
from chat_routes import chat_bp
|
| 54 |
+
from ui.layout import build_ui
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def create_app():
|
| 58 |
+
app = Flask(__name__)
|
| 59 |
+
|
| 60 |
+
app.config["SECRET_KEY"] = config.SECRET_KEY
|
| 61 |
+
app.config["SQLALCHEMY_DATABASE_URI"] = config.SQLALCHEMY_DATABASE_URI
|
| 62 |
+
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = config.SQLALCHEMY_TRACK_MODIFICATIONS
|
| 63 |
+
|
| 64 |
+
init_db(app)
|
| 65 |
+
|
| 66 |
+
app.register_blueprint(auth_bp)
|
| 67 |
+
app.register_blueprint(chat_bp)
|
| 68 |
+
|
| 69 |
+
@app.route("/health")
|
| 70 |
+
def health():
|
| 71 |
+
return {"status": "ok"}
|
| 72 |
+
|
| 73 |
+
return app
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
if __name__ == "__main__":
|
| 77 |
+
flask_app = create_app()
|
| 78 |
+
|
| 79 |
+
# Run the Flask REST API (auth + chat endpoints) on a background thread.
|
| 80 |
+
api_thread = threading.Thread(
|
| 81 |
+
target=lambda: flask_app.run(port=5000, debug=False, use_reloader=False),
|
| 82 |
+
daemon=True,
|
| 83 |
+
)
|
| 84 |
+
api_thread.start()
|
| 85 |
+
|
| 86 |
+
# Gradio UI is the main process. Its callbacks call into Flask
|
| 87 |
+
# in-process via app_context() (see ui/callbacks.py) rather than HTTP.
|
| 88 |
+
demo = build_ui(flask_app)
|
| 89 |
+
demo.queue().launch(
|
| 90 |
+
server_name="0.0.0.0",
|
| 91 |
+
server_port=7860,
|
| 92 |
+
show_api=False,
|
| 93 |
+
inbrowser=False,
|
| 94 |
+
)
|
auth/__init__.py
ADDED
|
File without changes
|
auth/db.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Shared SQLAlchemy instance.
|
| 3 |
+
|
| 4 |
+
Kept in its own module (rather than inside models.py or app.py) so that
|
| 5 |
+
every part of the application — auth, history, etc. — can import the same
|
| 6 |
+
`db` object without circular imports.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from flask_sqlalchemy import SQLAlchemy
|
| 10 |
+
|
| 11 |
+
db = SQLAlchemy()
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def init_db(app):
|
| 15 |
+
"""Bind SQLAlchemy to the Flask app and create tables if they don't exist."""
|
| 16 |
+
db.init_app(app)
|
| 17 |
+
with app.app_context():
|
| 18 |
+
# Import models here so they're registered on db.metadata before create_all()
|
| 19 |
+
from auth.models import User # noqa: F401
|
| 20 |
+
from history.models import ChatSession, ChatMessage # noqa: F401
|
| 21 |
+
|
| 22 |
+
db.create_all()
|
auth/models.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
User model for email/password authentication.
|
| 3 |
+
|
| 4 |
+
No OAuth. No plaintext passwords — only Werkzeug password hashes are stored.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
|
| 9 |
+
from werkzeug.security import generate_password_hash, check_password_hash
|
| 10 |
+
|
| 11 |
+
from auth.db import db
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class User(db.Model):
|
| 15 |
+
__tablename__ = "users"
|
| 16 |
+
|
| 17 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 18 |
+
email = db.Column(db.String(255), unique=True, nullable=False, index=True)
|
| 19 |
+
password_hash = db.Column(db.String(255), nullable=False)
|
| 20 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
| 21 |
+
|
| 22 |
+
# Relationship to chat sessions (defined in history/models.py)
|
| 23 |
+
chat_sessions = db.relationship(
|
| 24 |
+
"ChatSession", backref="user", lazy=True, cascade="all, delete-orphan"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
def set_password(self, raw_password: str) -> None:
|
| 28 |
+
self.password_hash = generate_password_hash(raw_password)
|
| 29 |
+
|
| 30 |
+
def check_password(self, raw_password: str) -> bool:
|
| 31 |
+
return check_password_hash(self.password_hash, raw_password)
|
| 32 |
+
|
| 33 |
+
def to_dict(self) -> dict:
|
| 34 |
+
return {
|
| 35 |
+
"id": self.id,
|
| 36 |
+
"email": self.email,
|
| 37 |
+
"created_at": self.created_at.isoformat(),
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
def __repr__(self) -> str:
|
| 41 |
+
return f"<User id={self.id} email={self.email}>"
|
auth/routes.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Authentication REST endpoints.
|
| 3 |
+
|
| 4 |
+
Routes:
|
| 5 |
+
POST /signup -> create a new user account
|
| 6 |
+
POST /login -> start a session-cookie-based login
|
| 7 |
+
POST /logout -> clear the session
|
| 8 |
+
GET /me -> return the currently authenticated user, if any
|
| 9 |
+
|
| 10 |
+
Session-based auth only: Flask's signed session cookie stores the user id.
|
| 11 |
+
No tokens, no OAuth.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import re
|
| 15 |
+
|
| 16 |
+
from flask import Blueprint, request, jsonify, session
|
| 17 |
+
|
| 18 |
+
from auth.db import db
|
| 19 |
+
from auth.models import User
|
| 20 |
+
|
| 21 |
+
auth_bp = Blueprint("auth", __name__)
|
| 22 |
+
|
| 23 |
+
EMAIL_REGEX = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
| 24 |
+
MIN_PASSWORD_LENGTH = 8
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _current_user():
|
| 28 |
+
user_id = session.get("user_id")
|
| 29 |
+
if user_id is None:
|
| 30 |
+
return None
|
| 31 |
+
return User.query.get(user_id)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@auth_bp.route("/signup", methods=["POST"])
|
| 35 |
+
def signup():
|
| 36 |
+
data = request.get_json(silent=True) or {}
|
| 37 |
+
email = (data.get("email") or "").strip().lower()
|
| 38 |
+
password = data.get("password") or ""
|
| 39 |
+
|
| 40 |
+
if not email or not EMAIL_REGEX.match(email):
|
| 41 |
+
return jsonify({"error": "A valid email is required."}), 400
|
| 42 |
+
|
| 43 |
+
if len(password) < MIN_PASSWORD_LENGTH:
|
| 44 |
+
return jsonify({
|
| 45 |
+
"error": f"Password must be at least {MIN_PASSWORD_LENGTH} characters."
|
| 46 |
+
}), 400
|
| 47 |
+
|
| 48 |
+
if User.query.filter_by(email=email).first() is not None:
|
| 49 |
+
return jsonify({"error": "An account with this email already exists."}), 409
|
| 50 |
+
|
| 51 |
+
user = User(email=email)
|
| 52 |
+
user.set_password(password)
|
| 53 |
+
db.session.add(user)
|
| 54 |
+
db.session.commit()
|
| 55 |
+
|
| 56 |
+
session["user_id"] = user.id
|
| 57 |
+
|
| 58 |
+
return jsonify({"message": "Account created.", "user": user.to_dict()}), 201
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@auth_bp.route("/login", methods=["POST"])
|
| 62 |
+
def login():
|
| 63 |
+
data = request.get_json(silent=True) or {}
|
| 64 |
+
email = (data.get("email") or "").strip().lower()
|
| 65 |
+
password = data.get("password") or ""
|
| 66 |
+
|
| 67 |
+
user = User.query.filter_by(email=email).first()
|
| 68 |
+
if user is None or not user.check_password(password):
|
| 69 |
+
return jsonify({"error": "Invalid email or password."}), 401
|
| 70 |
+
|
| 71 |
+
session["user_id"] = user.id
|
| 72 |
+
|
| 73 |
+
return jsonify({"message": "Logged in.", "user": user.to_dict()}), 200
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@auth_bp.route("/logout", methods=["POST"])
|
| 77 |
+
def logout():
|
| 78 |
+
session.pop("user_id", None)
|
| 79 |
+
return jsonify({"message": "Logged out."}), 200
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
@auth_bp.route("/me", methods=["GET"])
|
| 83 |
+
def me():
|
| 84 |
+
user = _current_user()
|
| 85 |
+
if user is None:
|
| 86 |
+
return jsonify({"user": None}), 200
|
| 87 |
+
return jsonify({"user": user.to_dict()}), 200
|
chat_routes.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Chat + session-history REST endpoints.
|
| 3 |
+
|
| 4 |
+
Routes:
|
| 5 |
+
POST /chat -> send a message, get a response
|
| 6 |
+
GET /sessions -> list the logged-in user's chat sessions
|
| 7 |
+
GET /sessions/<id> -> full transcript of one session
|
| 8 |
+
DELETE /sessions/<id> -> delete a session
|
| 9 |
+
|
| 10 |
+
Anonymous users can POST /chat (no persistence, no session panel per spec)
|
| 11 |
+
but cannot access /sessions endpoints.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from flask import Blueprint, jsonify, request, session
|
| 15 |
+
|
| 16 |
+
from auth.models import User
|
| 17 |
+
from chat_service import handle_chat_message
|
| 18 |
+
from history import service as history_service
|
| 19 |
+
|
| 20 |
+
chat_bp = Blueprint("chat", __name__)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _current_user():
|
| 24 |
+
user_id = session.get("user_id")
|
| 25 |
+
if user_id is None:
|
| 26 |
+
return None
|
| 27 |
+
return User.query.get(user_id)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@chat_bp.route("/chat", methods=["POST"])
|
| 31 |
+
def chat():
|
| 32 |
+
data = request.get_json(silent=True) or {}
|
| 33 |
+
query = (data.get("query") or "").strip()
|
| 34 |
+
|
| 35 |
+
if not query:
|
| 36 |
+
return jsonify({"error": "query is required."}), 400
|
| 37 |
+
|
| 38 |
+
user = _current_user()
|
| 39 |
+
session_id = data.get("session_id")
|
| 40 |
+
anonymous_history = data.get("history") if user is None else None
|
| 41 |
+
|
| 42 |
+
result = handle_chat_message(
|
| 43 |
+
query=query,
|
| 44 |
+
user=user,
|
| 45 |
+
session_id=session_id,
|
| 46 |
+
anonymous_history=anonymous_history,
|
| 47 |
+
)
|
| 48 |
+
return jsonify(result), 200
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@chat_bp.route("/sessions", methods=["GET"])
|
| 52 |
+
def list_sessions():
|
| 53 |
+
user = _current_user()
|
| 54 |
+
if user is None:
|
| 55 |
+
return jsonify({"error": "Login required."}), 401
|
| 56 |
+
return jsonify({"sessions": history_service.list_sessions(user.id)}), 200
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@chat_bp.route("/sessions/<int:session_id>", methods=["GET"])
|
| 60 |
+
def get_session_route(session_id):
|
| 61 |
+
user = _current_user()
|
| 62 |
+
if user is None:
|
| 63 |
+
return jsonify({"error": "Login required."}), 401
|
| 64 |
+
|
| 65 |
+
full_session = history_service.get_full_session(session_id, user.id)
|
| 66 |
+
if full_session is None:
|
| 67 |
+
return jsonify({"error": "Session not found."}), 404
|
| 68 |
+
|
| 69 |
+
return jsonify({"session": full_session}), 200
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@chat_bp.route("/sessions/<int:session_id>", methods=["DELETE"])
|
| 73 |
+
def delete_session_route(session_id):
|
| 74 |
+
user = _current_user()
|
| 75 |
+
if user is None:
|
| 76 |
+
return jsonify({"error": "Login required."}), 401
|
| 77 |
+
|
| 78 |
+
deleted = history_service.delete_session(session_id, user.id)
|
| 79 |
+
if not deleted:
|
| 80 |
+
return jsonify({"error": "Session not found."}), 404
|
| 81 |
+
|
| 82 |
+
return jsonify({"message": "Session deleted."}), 200
|
chat_service.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Chat orchestration service.
|
| 3 |
+
|
| 4 |
+
This is the single place that wires together everything built in the
|
| 5 |
+
previous layers for one chat turn:
|
| 6 |
+
|
| 7 |
+
agents.router -> classify + retrieve context
|
| 8 |
+
llm.prompts -> build the right prompt for the route
|
| 9 |
+
llm.generate -> call the configured LLM provider
|
| 10 |
+
memory.mem0_client -> pull/store long-term preference memory (logged-in only)
|
| 11 |
+
history.service -> persist the transcript (logged-in only)
|
| 12 |
+
|
| 13 |
+
Kept separate from app.py so Flask routes stay thin, and so the same logic
|
| 14 |
+
can be reused by the Gradio UI layer later without going through HTTP.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import config
|
| 18 |
+
from agents.router import RouteType, route
|
| 19 |
+
from history import service as history_service
|
| 20 |
+
from history.session_manager import resolve_session
|
| 21 |
+
from llm.generate import generate
|
| 22 |
+
from llm.prompts import (
|
| 23 |
+
build_comparison_prompt,
|
| 24 |
+
build_followup_prompt,
|
| 25 |
+
build_reject_prompt,
|
| 26 |
+
build_single_prompt,
|
| 27 |
+
)
|
| 28 |
+
from logs.logger import get_logger
|
| 29 |
+
from memory.mem0_client import get_mem0_client
|
| 30 |
+
from rag.retriever import get_retriever
|
| 31 |
+
|
| 32 |
+
logger = get_logger(__name__)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _build_prompt(decision, mem0_context, recent_messages):
|
| 36 |
+
if decision.route_type == RouteType.SINGLE_TOPIC:
|
| 37 |
+
return build_single_prompt(
|
| 38 |
+
decision.single_chunks, mem0_context, recent_messages, decision.query
|
| 39 |
+
)
|
| 40 |
+
if decision.route_type == RouteType.COMPARISON:
|
| 41 |
+
return build_comparison_prompt(
|
| 42 |
+
decision.comparison_context, mem0_context, recent_messages, decision.query
|
| 43 |
+
)
|
| 44 |
+
if decision.route_type == RouteType.FOLLOWUP:
|
| 45 |
+
return build_followup_prompt(
|
| 46 |
+
decision.followup_context, mem0_context, recent_messages, decision.query
|
| 47 |
+
)
|
| 48 |
+
# OUT_OF_SCOPE
|
| 49 |
+
return build_reject_prompt(decision.query)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def handle_chat_message(
|
| 53 |
+
query: str,
|
| 54 |
+
user=None,
|
| 55 |
+
session_id: int = None,
|
| 56 |
+
anonymous_history: list = None,
|
| 57 |
+
) -> dict:
|
| 58 |
+
"""
|
| 59 |
+
Args:
|
| 60 |
+
query: the user's message.
|
| 61 |
+
user: the authenticated auth.models.User, or None if anonymous.
|
| 62 |
+
session_id: existing chat session id (logged-in users only, optional).
|
| 63 |
+
anonymous_history: client-managed recent history for anonymous users
|
| 64 |
+
(no server-side persistence — see spec: "when
|
| 65 |
+
not logged in, chat works normally, no
|
| 66 |
+
persistence, no history panel"). Ignored for
|
| 67 |
+
logged-in users, whose history is read from
|
| 68 |
+
SQLite instead.
|
| 69 |
+
|
| 70 |
+
Returns:
|
| 71 |
+
{
|
| 72 |
+
"response": "...",
|
| 73 |
+
"route_type": "single_topic" | "comparison" | "followup" | "out_of_scope",
|
| 74 |
+
"topics": [...],
|
| 75 |
+
"session_id": int | None, # None for anonymous users
|
| 76 |
+
}
|
| 77 |
+
"""
|
| 78 |
+
is_logged_in = user is not None
|
| 79 |
+
session_obj = None
|
| 80 |
+
|
| 81 |
+
if is_logged_in:
|
| 82 |
+
session_obj = resolve_session(user.id, session_id, first_message=query)
|
| 83 |
+
recent_messages = history_service.get_recent_messages(session_obj.id)
|
| 84 |
+
else:
|
| 85 |
+
recent_messages = (anonymous_history or [])[-config.MAX_RECENT_HISTORY_MESSAGES :]
|
| 86 |
+
|
| 87 |
+
retriever = get_retriever()
|
| 88 |
+
decision = route(query, retriever, recent_messages=recent_messages)
|
| 89 |
+
|
| 90 |
+
mem0_context = []
|
| 91 |
+
if is_logged_in:
|
| 92 |
+
mem0_context = get_mem0_client().get_relevant_context(str(user.id), query)
|
| 93 |
+
|
| 94 |
+
prompt = _build_prompt(decision, mem0_context, recent_messages)
|
| 95 |
+
response_text = generate(prompt)
|
| 96 |
+
|
| 97 |
+
if is_logged_in:
|
| 98 |
+
history_service.add_message(
|
| 99 |
+
session_obj.id,
|
| 100 |
+
role="user",
|
| 101 |
+
content=query,
|
| 102 |
+
route_type=decision.route_type.value,
|
| 103 |
+
retrieved_topics=decision.topics,
|
| 104 |
+
)
|
| 105 |
+
history_service.add_message(
|
| 106 |
+
session_obj.id,
|
| 107 |
+
role="assistant",
|
| 108 |
+
content=response_text,
|
| 109 |
+
route_type=decision.route_type.value,
|
| 110 |
+
)
|
| 111 |
+
get_mem0_client().add_interaction(str(user.id), query, response_text)
|
| 112 |
+
|
| 113 |
+
logger.info(
|
| 114 |
+
"Chat turn complete | user_id=%s | route=%s | topics=%s | session_id=%s",
|
| 115 |
+
user.id if is_logged_in else None,
|
| 116 |
+
decision.route_type.value,
|
| 117 |
+
decision.topics,
|
| 118 |
+
session_obj.id if session_obj else None,
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
return {
|
| 122 |
+
"response": response_text,
|
| 123 |
+
"route_type": decision.route_type.value,
|
| 124 |
+
"topics": decision.topics,
|
| 125 |
+
"session_id": session_obj.id if session_obj else None,
|
| 126 |
+
}
|
config.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Central configuration for the DSA RAG Chatbot.
|
| 3 |
+
No magic numbers should be hardcoded anywhere else in the codebase — import from here.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 12 |
+
|
| 13 |
+
# ---------------------------------------------------------------------------
|
| 14 |
+
# Flask / App
|
| 15 |
+
# ---------------------------------------------------------------------------
|
| 16 |
+
SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-key-change-me")
|
| 17 |
+
SESSION_COOKIE_NAME = "dsa_chatbot_session"
|
| 18 |
+
SESSION_LIFETIME_MINUTES = int(os.getenv("SESSION_LIFETIME_MINUTES", 60 * 24 * 7)) # 7 days
|
| 19 |
+
|
| 20 |
+
# ---------------------------------------------------------------------------
|
| 21 |
+
# Database (SQLite via SQLAlchemy)
|
| 22 |
+
# ---------------------------------------------------------------------------
|
| 23 |
+
DATABASE_PATH = os.path.join(BASE_DIR, "database", "app.db")
|
| 24 |
+
SQLALCHEMY_DATABASE_URI = f"sqlite:///{DATABASE_PATH}"
|
| 25 |
+
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
| 26 |
+
|
| 27 |
+
# ---------------------------------------------------------------------------
|
| 28 |
+
# RAG / Knowledge base
|
| 29 |
+
# ---------------------------------------------------------------------------
|
| 30 |
+
KNOWLEDGE_BASE_DIR = os.path.join(BASE_DIR, "rag", "knowledge_base")
|
| 31 |
+
TOPIC_METADATA_PATH = os.path.join(BASE_DIR, "rag", "knowledge_base", "topic_metadata.json")
|
| 32 |
+
CHROMA_DB_DIR = os.path.join(BASE_DIR, "rag", "chroma_db")
|
| 33 |
+
CHROMA_COLLECTION_NAME = "dsa_knowledge_base"
|
| 34 |
+
|
| 35 |
+
EMBEDDING_MODEL_NAME = "all-MiniLM-L6-v2"
|
| 36 |
+
|
| 37 |
+
# Sections expected inside each knowledge-base .txt file. Used by the chunker
|
| 38 |
+
# to split on structure rather than arbitrary fixed-size windows.
|
| 39 |
+
KB_SECTION_HEADERS = [
|
| 40 |
+
"DEFINITION",
|
| 41 |
+
"TIME_COMPLEXITY",
|
| 42 |
+
"SPACE_COMPLEXITY",
|
| 43 |
+
"USE_WHEN",
|
| 44 |
+
"EXAMPLE",
|
| 45 |
+
]
|
| 46 |
+
|
| 47 |
+
# ---------------------------------------------------------------------------
|
| 48 |
+
# Retrieval pipeline
|
| 49 |
+
# ---------------------------------------------------------------------------
|
| 50 |
+
INITIAL_RETRIEVAL_K = int(os.getenv("INITIAL_RETRIEVAL_K", 15))
|
| 51 |
+
TOP_K = int(os.getenv("TOP_K", 5))
|
| 52 |
+
SIMILARITY_THRESHOLD = float(os.getenv("SIMILARITY_THRESHOLD", 0.45))
|
| 53 |
+
|
| 54 |
+
# ---------------------------------------------------------------------------
|
| 55 |
+
# Memory (mem0)
|
| 56 |
+
# ---------------------------------------------------------------------------
|
| 57 |
+
USE_MEM0 = os.getenv("USE_MEM0", "True") == "True"
|
| 58 |
+
MEM0_LOCAL_STORAGE_DIR = os.path.join(BASE_DIR, "memory", "mem0_store")
|
| 59 |
+
|
| 60 |
+
# ---------------------------------------------------------------------------
|
| 61 |
+
# History / conversation
|
| 62 |
+
# ---------------------------------------------------------------------------
|
| 63 |
+
MAX_RECENT_HISTORY_MESSAGES = int(os.getenv("MAX_RECENT_HISTORY_MESSAGES", 6))
|
| 64 |
+
|
| 65 |
+
# ---------------------------------------------------------------------------
|
| 66 |
+
# LLM Provider
|
| 67 |
+
# ---------------------------------------------------------------------------
|
| 68 |
+
# "local" -> Mistral-7B-Instruct-v0.2 via transformers/bitsandbytes
|
| 69 |
+
# "groq" -> future Groq-hosted provider
|
| 70 |
+
# "openai"-> future OpenAI provider
|
| 71 |
+
# LLM_PROVIDER = os.getenv("LLM_PROVIDER", "local")
|
| 72 |
+
LLM_PROVIDER = os.getenv("LLM_PROVIDER", "groq")
|
| 73 |
+
|
| 74 |
+
LOCAL_MODEL_NAME = os.getenv("LOCAL_MODEL_NAME", "mistralai/Mistral-7B-Instruct-v0.2")
|
| 75 |
+
LOCAL_MODEL_MAX_NEW_TOKENS = int(os.getenv("LOCAL_MODEL_MAX_NEW_TOKENS", 512))
|
| 76 |
+
LOCAL_MODEL_TEMPERATURE = float(os.getenv("LOCAL_MODEL_TEMPERATURE", 0.3))
|
| 77 |
+
LOCAL_MODEL_LOAD_IN_4BIT = os.getenv("LOCAL_MODEL_LOAD_IN_4BIT", "True") == "True"
|
| 78 |
+
|
| 79 |
+
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
|
| 80 |
+
GROQ_MODEL_NAME = os.getenv("GROQ_MODEL_NAME", "llama-3.3-70b-versatile")
|
| 81 |
+
|
| 82 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
|
| 83 |
+
OPENAI_MODEL_NAME = os.getenv("OPENAI_MODEL_NAME", "")
|
| 84 |
+
|
| 85 |
+
# ---------------------------------------------------------------------------
|
| 86 |
+
# Logging
|
| 87 |
+
# ---------------------------------------------------------------------------
|
| 88 |
+
LOG_DIR = os.path.join(BASE_DIR, "logs")
|
| 89 |
+
LOG_FILE_PATH = os.path.join(LOG_DIR, "app.log")
|
| 90 |
+
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
|
database/__init__.py
ADDED
|
File without changes
|
database/app.db
ADDED
|
Binary file (41 kB). View file
|
|
|
evaluation/__init__.py
ADDED
|
File without changes
|
evaluation/evaluate.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Evaluation harness for the DSA RAG chatbot.
|
| 3 |
+
|
| 4 |
+
Runs the predefined question set (evaluation/questions.json) through the
|
| 5 |
+
agent router + retriever (and optionally the full LLM pipeline) and reports:
|
| 6 |
+
|
| 7 |
+
- routing accuracy (predicted route_type vs expected_route)
|
| 8 |
+
- retrieval topic recall (expected topics found among retrieved chunks)
|
| 9 |
+
- retrieval latency (avg / p95, ms)
|
| 10 |
+
- generation latency (avg / p95, ms) -- only with --with-llm
|
| 11 |
+
|
| 12 |
+
Usage:
|
| 13 |
+
python -m evaluation.evaluate
|
| 14 |
+
python -m evaluation.evaluate --with-llm
|
| 15 |
+
python -m evaluation.evaluate --questions evaluation/questions.json
|
| 16 |
+
|
| 17 |
+
Run from the project root (dsa-rag-chatbot/) so `config` and the app
|
| 18 |
+
packages resolve correctly.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
import argparse
|
| 22 |
+
import json
|
| 23 |
+
import os
|
| 24 |
+
import statistics
|
| 25 |
+
import time
|
| 26 |
+
|
| 27 |
+
import config
|
| 28 |
+
from agents.followup import gather_followup_context
|
| 29 |
+
from agents.router import RouteType, classify, gather_context
|
| 30 |
+
from logs.logger import get_logger
|
| 31 |
+
|
| 32 |
+
logger = get_logger(__name__)
|
| 33 |
+
|
| 34 |
+
DEFAULT_QUESTIONS_PATH = os.path.join(os.path.dirname(__file__), "questions.json")
|
| 35 |
+
|
| 36 |
+
# A tiny bit of fake prior conversation so the follow-up route can actually
|
| 37 |
+
# be exercised (is_followup_query requires has_conversation_history=True).
|
| 38 |
+
_FAKE_HISTORY_FOR_FOLLOWUPS = [
|
| 39 |
+
{"role": "user", "content": "Explain merge sort"},
|
| 40 |
+
{"role": "assistant", "content": "Merge sort is a divide-and-conquer sorting algorithm..."},
|
| 41 |
+
]
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _load_questions(path: str) -> list:
|
| 45 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 46 |
+
return json.load(f)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _percentile(values: list, pct: float) -> float:
|
| 50 |
+
if not values:
|
| 51 |
+
return 0.0
|
| 52 |
+
values = sorted(values)
|
| 53 |
+
k = (len(values) - 1) * (pct / 100)
|
| 54 |
+
f, c = int(k), min(int(k) + 1, len(values) - 1)
|
| 55 |
+
if f == c:
|
| 56 |
+
return values[f]
|
| 57 |
+
return values[f] + (k - f) * (values[c] - values[f])
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _retrieved_topics(chunks: list) -> set:
|
| 61 |
+
return {c.get("metadata", {}).get("topic") for c in chunks if c.get("metadata", {}).get("topic")}
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _topic_recall(expected_topics: list, retrieved: set) -> float:
|
| 65 |
+
if not expected_topics:
|
| 66 |
+
return 1.0 # nothing expected (e.g. out_of_scope) => trivially satisfied
|
| 67 |
+
hits = sum(1 for t in expected_topics if t in retrieved)
|
| 68 |
+
return hits / len(expected_topics)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def evaluate_one(item: dict, retriever, with_llm: bool) -> dict:
|
| 72 |
+
query = item["query"]
|
| 73 |
+
expected_route = item["expected_route"]
|
| 74 |
+
expected_topics = item.get("expected_topics", [])
|
| 75 |
+
recent_messages = _FAKE_HISTORY_FOR_FOLLOWUPS if item.get("requires_history") else []
|
| 76 |
+
|
| 77 |
+
t0 = time.perf_counter()
|
| 78 |
+
decision = classify(query, has_conversation_history=len(recent_messages) > 0)
|
| 79 |
+
decision = gather_context(decision, retriever, recent_messages=recent_messages)
|
| 80 |
+
retrieval_latency_ms = (time.perf_counter() - t0) * 1000
|
| 81 |
+
|
| 82 |
+
if decision.route_type == RouteType.SINGLE_TOPIC:
|
| 83 |
+
retrieved = _retrieved_topics(decision.single_chunks)
|
| 84 |
+
elif decision.route_type == RouteType.COMPARISON:
|
| 85 |
+
retrieved = set()
|
| 86 |
+
for chunks in decision.comparison_context.values():
|
| 87 |
+
retrieved |= _retrieved_topics(chunks)
|
| 88 |
+
elif decision.route_type == RouteType.FOLLOWUP:
|
| 89 |
+
retrieved = _retrieved_topics(decision.followup_context.get("additional_chunks", []))
|
| 90 |
+
else:
|
| 91 |
+
retrieved = set()
|
| 92 |
+
|
| 93 |
+
route_correct = decision.route_type.value == expected_route
|
| 94 |
+
topic_recall = _topic_recall(expected_topics, retrieved)
|
| 95 |
+
|
| 96 |
+
result = {
|
| 97 |
+
"id": item.get("id"),
|
| 98 |
+
"query": query,
|
| 99 |
+
"expected_route": expected_route,
|
| 100 |
+
"predicted_route": decision.route_type.value,
|
| 101 |
+
"route_correct": route_correct,
|
| 102 |
+
"expected_topics": expected_topics,
|
| 103 |
+
"retrieved_topics": sorted(t for t in retrieved if t),
|
| 104 |
+
"topic_recall": round(topic_recall, 3),
|
| 105 |
+
"retrieval_latency_ms": round(retrieval_latency_ms, 2),
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
if with_llm:
|
| 109 |
+
from llm.generate import generate
|
| 110 |
+
from llm.prompts import (
|
| 111 |
+
build_comparison_prompt,
|
| 112 |
+
build_followup_prompt,
|
| 113 |
+
build_reject_prompt,
|
| 114 |
+
build_single_prompt,
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
if decision.route_type == RouteType.SINGLE_TOPIC:
|
| 118 |
+
prompt = build_single_prompt(decision.single_chunks, [], recent_messages, query)
|
| 119 |
+
elif decision.route_type == RouteType.COMPARISON:
|
| 120 |
+
prompt = build_comparison_prompt(decision.comparison_context, [], recent_messages, query)
|
| 121 |
+
elif decision.route_type == RouteType.FOLLOWUP:
|
| 122 |
+
prompt = build_followup_prompt(decision.followup_context, [], recent_messages, query)
|
| 123 |
+
else:
|
| 124 |
+
prompt = build_reject_prompt(query)
|
| 125 |
+
|
| 126 |
+
t1 = time.perf_counter()
|
| 127 |
+
try:
|
| 128 |
+
response_text = generate(prompt)
|
| 129 |
+
gen_error = None
|
| 130 |
+
except Exception as exc: # keep the run going even if the LLM call fails
|
| 131 |
+
response_text = None
|
| 132 |
+
gen_error = str(exc)
|
| 133 |
+
generation_latency_ms = (time.perf_counter() - t1) * 1000
|
| 134 |
+
|
| 135 |
+
result["generation_latency_ms"] = round(generation_latency_ms, 2)
|
| 136 |
+
result["response_preview"] = (response_text or "")[:200]
|
| 137 |
+
if gen_error:
|
| 138 |
+
result["generation_error"] = gen_error
|
| 139 |
+
|
| 140 |
+
return result
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def run(questions_path: str = DEFAULT_QUESTIONS_PATH, with_llm: bool = False) -> dict:
|
| 144 |
+
from rag.retriever import get_retriever
|
| 145 |
+
|
| 146 |
+
questions = _load_questions(questions_path)
|
| 147 |
+
retriever = get_retriever()
|
| 148 |
+
|
| 149 |
+
results = [evaluate_one(item, retriever, with_llm) for item in questions]
|
| 150 |
+
|
| 151 |
+
route_accuracy = sum(r["route_correct"] for r in results) / len(results)
|
| 152 |
+
avg_topic_recall = statistics.mean(r["topic_recall"] for r in results)
|
| 153 |
+
retrieval_latencies = [r["retrieval_latency_ms"] for r in results]
|
| 154 |
+
|
| 155 |
+
summary = {
|
| 156 |
+
"total_questions": len(results),
|
| 157 |
+
"route_accuracy": round(route_accuracy, 3),
|
| 158 |
+
"avg_topic_recall": round(avg_topic_recall, 3),
|
| 159 |
+
"retrieval_latency_ms_avg": round(statistics.mean(retrieval_latencies), 2),
|
| 160 |
+
"retrieval_latency_ms_p95": round(_percentile(retrieval_latencies, 95), 2),
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
if with_llm:
|
| 164 |
+
gen_latencies = [r["generation_latency_ms"] for r in results if "generation_latency_ms" in r]
|
| 165 |
+
if gen_latencies:
|
| 166 |
+
summary["generation_latency_ms_avg"] = round(statistics.mean(gen_latencies), 2)
|
| 167 |
+
summary["generation_latency_ms_p95"] = round(_percentile(gen_latencies, 95), 2)
|
| 168 |
+
summary["generation_errors"] = sum(1 for r in results if r.get("generation_error"))
|
| 169 |
+
|
| 170 |
+
report = {"summary": summary, "results": results}
|
| 171 |
+
|
| 172 |
+
logger.info("Evaluation complete: %s", summary)
|
| 173 |
+
return report
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def _print_report(report: dict) -> None:
|
| 177 |
+
summary = report["summary"]
|
| 178 |
+
print("\n=== DSA RAG Chatbot — Evaluation Summary ===")
|
| 179 |
+
for key, value in summary.items():
|
| 180 |
+
print(f" {key}: {value}")
|
| 181 |
+
|
| 182 |
+
print("\n=== Per-question results ===")
|
| 183 |
+
header = f"{'id':<5} {'route (exp->got)':<28} {'recall':<8} {'ret_ms':<8} query"
|
| 184 |
+
print(header)
|
| 185 |
+
print("-" * len(header))
|
| 186 |
+
for r in report["results"]:
|
| 187 |
+
route_str = f"{r['expected_route']} -> {r['predicted_route']}"
|
| 188 |
+
mark = "OK" if r["route_correct"] else "MISS"
|
| 189 |
+
print(
|
| 190 |
+
f"{r['id']:<5} {route_str:<28} {r['topic_recall']:<8} "
|
| 191 |
+
f"{r['retrieval_latency_ms']:<8} [{mark}] {r['query']}"
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def main():
|
| 196 |
+
parser = argparse.ArgumentParser(description="Evaluate DSA RAG chatbot retrieval + routing.")
|
| 197 |
+
parser.add_argument(
|
| 198 |
+
"--questions", default=DEFAULT_QUESTIONS_PATH, help="Path to questions.json"
|
| 199 |
+
)
|
| 200 |
+
parser.add_argument(
|
| 201 |
+
"--with-llm",
|
| 202 |
+
action="store_true",
|
| 203 |
+
help="Also call the configured LLM provider end-to-end (uses API quota).",
|
| 204 |
+
)
|
| 205 |
+
parser.add_argument(
|
| 206 |
+
"--out",
|
| 207 |
+
default=None,
|
| 208 |
+
help="Path to write the full JSON report (default: evaluation/last_report.json)",
|
| 209 |
+
)
|
| 210 |
+
args = parser.parse_args()
|
| 211 |
+
|
| 212 |
+
report = run(questions_path=args.questions, with_llm=args.with_llm)
|
| 213 |
+
_print_report(report)
|
| 214 |
+
|
| 215 |
+
out_path = args.out or os.path.join(os.path.dirname(__file__), "last_report.json")
|
| 216 |
+
with open(out_path, "w", encoding="utf-8") as f:
|
| 217 |
+
json.dump(report, f, indent=2)
|
| 218 |
+
print(f"\nFull report written to: {out_path}")
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
if __name__ == "__main__":
|
| 222 |
+
main()
|
evaluation/last_report.json
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"summary": {
|
| 3 |
+
"total_questions": 20,
|
| 4 |
+
"route_accuracy": 1.0,
|
| 5 |
+
"avg_topic_recall": 1.0,
|
| 6 |
+
"retrieval_latency_ms_avg": 46.26,
|
| 7 |
+
"retrieval_latency_ms_p95": 101.1
|
| 8 |
+
},
|
| 9 |
+
"results": [
|
| 10 |
+
{
|
| 11 |
+
"id": "q01",
|
| 12 |
+
"query": "Explain how binary search works",
|
| 13 |
+
"expected_route": "single_topic",
|
| 14 |
+
"predicted_route": "single_topic",
|
| 15 |
+
"route_correct": true,
|
| 16 |
+
"expected_topics": [
|
| 17 |
+
"Binary Search"
|
| 18 |
+
],
|
| 19 |
+
"retrieved_topics": [
|
| 20 |
+
"Binary Search"
|
| 21 |
+
],
|
| 22 |
+
"topic_recall": 1.0,
|
| 23 |
+
"retrieval_latency_ms": 144.79
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"id": "q02",
|
| 27 |
+
"query": "What is the time complexity of merge sort?",
|
| 28 |
+
"expected_route": "single_topic",
|
| 29 |
+
"predicted_route": "single_topic",
|
| 30 |
+
"route_correct": true,
|
| 31 |
+
"expected_topics": [
|
| 32 |
+
"Merge Sort"
|
| 33 |
+
],
|
| 34 |
+
"retrieved_topics": [
|
| 35 |
+
"Merge Sort"
|
| 36 |
+
],
|
| 37 |
+
"topic_recall": 1.0,
|
| 38 |
+
"retrieval_latency_ms": 30.63
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"id": "q03",
|
| 42 |
+
"query": "Explain quick sort with an example",
|
| 43 |
+
"expected_route": "single_topic",
|
| 44 |
+
"predicted_route": "single_topic",
|
| 45 |
+
"route_correct": true,
|
| 46 |
+
"expected_topics": [
|
| 47 |
+
"Quick Sort"
|
| 48 |
+
],
|
| 49 |
+
"retrieved_topics": [
|
| 50 |
+
"Quick Sort"
|
| 51 |
+
],
|
| 52 |
+
"topic_recall": 1.0,
|
| 53 |
+
"retrieval_latency_ms": 33.24
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"id": "q04",
|
| 57 |
+
"query": "What is a Trie used for?",
|
| 58 |
+
"expected_route": "single_topic",
|
| 59 |
+
"predicted_route": "single_topic",
|
| 60 |
+
"route_correct": true,
|
| 61 |
+
"expected_topics": [
|
| 62 |
+
"Trie"
|
| 63 |
+
],
|
| 64 |
+
"retrieved_topics": [
|
| 65 |
+
"Trie"
|
| 66 |
+
],
|
| 67 |
+
"topic_recall": 1.0,
|
| 68 |
+
"retrieval_latency_ms": 31.99
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"id": "q05",
|
| 72 |
+
"query": "What is a doubly linked list?",
|
| 73 |
+
"expected_route": "single_topic",
|
| 74 |
+
"predicted_route": "single_topic",
|
| 75 |
+
"route_correct": true,
|
| 76 |
+
"expected_topics": [
|
| 77 |
+
"Doubly Linked List"
|
| 78 |
+
],
|
| 79 |
+
"retrieved_topics": [
|
| 80 |
+
"Doubly Linked List"
|
| 81 |
+
],
|
| 82 |
+
"topic_recall": 1.0,
|
| 83 |
+
"retrieval_latency_ms": 34.44
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
"id": "q06",
|
| 87 |
+
"query": "Explain how a stack works",
|
| 88 |
+
"expected_route": "single_topic",
|
| 89 |
+
"predicted_route": "single_topic",
|
| 90 |
+
"route_correct": true,
|
| 91 |
+
"expected_topics": [
|
| 92 |
+
"Stack"
|
| 93 |
+
],
|
| 94 |
+
"retrieved_topics": [
|
| 95 |
+
"Stack"
|
| 96 |
+
],
|
| 97 |
+
"topic_recall": 1.0,
|
| 98 |
+
"retrieval_latency_ms": 28.98
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"id": "q07",
|
| 102 |
+
"query": "What is topological sort used for?",
|
| 103 |
+
"expected_route": "single_topic",
|
| 104 |
+
"predicted_route": "single_topic",
|
| 105 |
+
"route_correct": true,
|
| 106 |
+
"expected_topics": [
|
| 107 |
+
"Topological Sort"
|
| 108 |
+
],
|
| 109 |
+
"retrieved_topics": [
|
| 110 |
+
"Topological Sort"
|
| 111 |
+
],
|
| 112 |
+
"topic_recall": 1.0,
|
| 113 |
+
"retrieval_latency_ms": 33.71
|
| 114 |
+
},
|
| 115 |
+
{
|
| 116 |
+
"id": "q08",
|
| 117 |
+
"query": "Explain Dijkstra's Algorithm",
|
| 118 |
+
"expected_route": "single_topic",
|
| 119 |
+
"predicted_route": "single_topic",
|
| 120 |
+
"route_correct": true,
|
| 121 |
+
"expected_topics": [
|
| 122 |
+
"Dijkstra's Algorithm"
|
| 123 |
+
],
|
| 124 |
+
"retrieved_topics": [
|
| 125 |
+
"Dijkstra's Algorithm"
|
| 126 |
+
],
|
| 127 |
+
"topic_recall": 1.0,
|
| 128 |
+
"retrieval_latency_ms": 27.78
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
"id": "q09",
|
| 132 |
+
"query": "What is a segment tree?",
|
| 133 |
+
"expected_route": "single_topic",
|
| 134 |
+
"predicted_route": "single_topic",
|
| 135 |
+
"route_correct": true,
|
| 136 |
+
"expected_topics": [
|
| 137 |
+
"Segment Tree"
|
| 138 |
+
],
|
| 139 |
+
"retrieved_topics": [
|
| 140 |
+
"Segment Tree"
|
| 141 |
+
],
|
| 142 |
+
"topic_recall": 1.0,
|
| 143 |
+
"retrieval_latency_ms": 35.57
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
"id": "q10",
|
| 147 |
+
"query": "Explain the sliding window technique",
|
| 148 |
+
"expected_route": "single_topic",
|
| 149 |
+
"predicted_route": "single_topic",
|
| 150 |
+
"route_correct": true,
|
| 151 |
+
"expected_topics": [
|
| 152 |
+
"Sliding Window Technique"
|
| 153 |
+
],
|
| 154 |
+
"retrieved_topics": [
|
| 155 |
+
"Sliding Window Technique"
|
| 156 |
+
],
|
| 157 |
+
"topic_recall": 1.0,
|
| 158 |
+
"retrieval_latency_ms": 34.67
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
"id": "q11",
|
| 162 |
+
"query": "What is the difference between BFS and DFS?",
|
| 163 |
+
"expected_route": "comparison",
|
| 164 |
+
"predicted_route": "comparison",
|
| 165 |
+
"route_correct": true,
|
| 166 |
+
"expected_topics": [
|
| 167 |
+
"Breadth-First Search",
|
| 168 |
+
"Depth-First Search"
|
| 169 |
+
],
|
| 170 |
+
"retrieved_topics": [
|
| 171 |
+
"Breadth-First Search",
|
| 172 |
+
"Depth-First Search"
|
| 173 |
+
],
|
| 174 |
+
"topic_recall": 1.0,
|
| 175 |
+
"retrieval_latency_ms": 51.73
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
"id": "q12",
|
| 179 |
+
"query": "Compare merge sort vs quick sort",
|
| 180 |
+
"expected_route": "comparison",
|
| 181 |
+
"predicted_route": "comparison",
|
| 182 |
+
"route_correct": true,
|
| 183 |
+
"expected_topics": [
|
| 184 |
+
"Merge Sort",
|
| 185 |
+
"Quick Sort"
|
| 186 |
+
],
|
| 187 |
+
"retrieved_topics": [
|
| 188 |
+
"Merge Sort",
|
| 189 |
+
"Quick Sort"
|
| 190 |
+
],
|
| 191 |
+
"topic_recall": 1.0,
|
| 192 |
+
"retrieval_latency_ms": 63.2
|
| 193 |
+
},
|
| 194 |
+
{
|
| 195 |
+
"id": "q13",
|
| 196 |
+
"query": "Difference between a stack and a queue",
|
| 197 |
+
"expected_route": "comparison",
|
| 198 |
+
"predicted_route": "comparison",
|
| 199 |
+
"route_correct": true,
|
| 200 |
+
"expected_topics": [
|
| 201 |
+
"Stack",
|
| 202 |
+
"Queue"
|
| 203 |
+
],
|
| 204 |
+
"retrieved_topics": [
|
| 205 |
+
"Queue",
|
| 206 |
+
"Stack"
|
| 207 |
+
],
|
| 208 |
+
"topic_recall": 1.0,
|
| 209 |
+
"retrieval_latency_ms": 66.79
|
| 210 |
+
},
|
| 211 |
+
{
|
| 212 |
+
"id": "q14",
|
| 213 |
+
"query": "Which one is better, Prim's Algorithm or Kruskal's Algorithm, for MST?",
|
| 214 |
+
"expected_route": "comparison",
|
| 215 |
+
"predicted_route": "comparison",
|
| 216 |
+
"route_correct": true,
|
| 217 |
+
"expected_topics": [
|
| 218 |
+
"Prim's Algorithm (MST)",
|
| 219 |
+
"Kruskal's Algorithm (MST)"
|
| 220 |
+
],
|
| 221 |
+
"retrieved_topics": [
|
| 222 |
+
"Kruskal's Algorithm (MST)",
|
| 223 |
+
"Prim's Algorithm (MST)"
|
| 224 |
+
],
|
| 225 |
+
"topic_recall": 1.0,
|
| 226 |
+
"retrieval_latency_ms": 98.8
|
| 227 |
+
},
|
| 228 |
+
{
|
| 229 |
+
"id": "q15",
|
| 230 |
+
"query": "Compare AVL Tree and Binary Search Tree",
|
| 231 |
+
"expected_route": "comparison",
|
| 232 |
+
"predicted_route": "comparison",
|
| 233 |
+
"route_correct": true,
|
| 234 |
+
"expected_topics": [
|
| 235 |
+
"AVL Tree",
|
| 236 |
+
"Binary Search Tree"
|
| 237 |
+
],
|
| 238 |
+
"retrieved_topics": [
|
| 239 |
+
"AVL Tree",
|
| 240 |
+
"Binary Search",
|
| 241 |
+
"Binary Search Tree"
|
| 242 |
+
],
|
| 243 |
+
"topic_recall": 1.0,
|
| 244 |
+
"retrieval_latency_ms": 84.79
|
| 245 |
+
},
|
| 246 |
+
{
|
| 247 |
+
"id": "q16",
|
| 248 |
+
"query": "Who won the football World Cup in 2022?",
|
| 249 |
+
"expected_route": "out_of_scope",
|
| 250 |
+
"predicted_route": "out_of_scope",
|
| 251 |
+
"route_correct": true,
|
| 252 |
+
"expected_topics": [],
|
| 253 |
+
"retrieved_topics": [],
|
| 254 |
+
"topic_recall": 1.0,
|
| 255 |
+
"retrieval_latency_ms": 23.85
|
| 256 |
+
},
|
| 257 |
+
{
|
| 258 |
+
"id": "q17",
|
| 259 |
+
"query": "What's a good recipe for chocolate cake?",
|
| 260 |
+
"expected_route": "out_of_scope",
|
| 261 |
+
"predicted_route": "out_of_scope",
|
| 262 |
+
"route_correct": true,
|
| 263 |
+
"expected_topics": [],
|
| 264 |
+
"retrieved_topics": [],
|
| 265 |
+
"topic_recall": 1.0,
|
| 266 |
+
"retrieval_latency_ms": 16.91
|
| 267 |
+
},
|
| 268 |
+
{
|
| 269 |
+
"id": "q18",
|
| 270 |
+
"query": "What is the capital of France?",
|
| 271 |
+
"expected_route": "out_of_scope",
|
| 272 |
+
"predicted_route": "out_of_scope",
|
| 273 |
+
"route_correct": true,
|
| 274 |
+
"expected_topics": [],
|
| 275 |
+
"retrieved_topics": [],
|
| 276 |
+
"topic_recall": 1.0,
|
| 277 |
+
"retrieval_latency_ms": 14.83
|
| 278 |
+
},
|
| 279 |
+
{
|
| 280 |
+
"id": "q19",
|
| 281 |
+
"query": "Can you explain that again?",
|
| 282 |
+
"expected_route": "followup",
|
| 283 |
+
"predicted_route": "followup",
|
| 284 |
+
"route_correct": true,
|
| 285 |
+
"expected_topics": [],
|
| 286 |
+
"retrieved_topics": [],
|
| 287 |
+
"topic_recall": 1.0,
|
| 288 |
+
"retrieval_latency_ms": 44.24
|
| 289 |
+
},
|
| 290 |
+
{
|
| 291 |
+
"id": "q20",
|
| 292 |
+
"query": "Give me another example",
|
| 293 |
+
"expected_route": "followup",
|
| 294 |
+
"predicted_route": "followup",
|
| 295 |
+
"route_correct": true,
|
| 296 |
+
"expected_topics": [],
|
| 297 |
+
"retrieved_topics": [],
|
| 298 |
+
"topic_recall": 1.0,
|
| 299 |
+
"retrieval_latency_ms": 24.32
|
| 300 |
+
}
|
| 301 |
+
]
|
| 302 |
+
}
|
evaluation/questions.json
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"id": "q01",
|
| 4 |
+
"query": "Explain how binary search works",
|
| 5 |
+
"expected_route": "single_topic",
|
| 6 |
+
"expected_topics": ["Binary Search"]
|
| 7 |
+
},
|
| 8 |
+
{
|
| 9 |
+
"id": "q02",
|
| 10 |
+
"query": "What is the time complexity of merge sort?",
|
| 11 |
+
"expected_route": "single_topic",
|
| 12 |
+
"expected_topics": ["Merge Sort"]
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"id": "q03",
|
| 16 |
+
"query": "Explain quick sort with an example",
|
| 17 |
+
"expected_route": "single_topic",
|
| 18 |
+
"expected_topics": ["Quick Sort"]
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"id": "q04",
|
| 22 |
+
"query": "What is a Trie used for?",
|
| 23 |
+
"expected_route": "single_topic",
|
| 24 |
+
"expected_topics": ["Trie"]
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "q05",
|
| 28 |
+
"query": "What is a doubly linked list?",
|
| 29 |
+
"expected_route": "single_topic",
|
| 30 |
+
"expected_topics": ["Doubly Linked List"]
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"id": "q06",
|
| 34 |
+
"query": "Explain how a stack works",
|
| 35 |
+
"expected_route": "single_topic",
|
| 36 |
+
"expected_topics": ["Stack"]
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"id": "q07",
|
| 40 |
+
"query": "What is topological sort used for?",
|
| 41 |
+
"expected_route": "single_topic",
|
| 42 |
+
"expected_topics": ["Topological Sort"]
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"id": "q08",
|
| 46 |
+
"query": "Explain Dijkstra's Algorithm",
|
| 47 |
+
"expected_route": "single_topic",
|
| 48 |
+
"expected_topics": ["Dijkstra's Algorithm"]
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"id": "q09",
|
| 52 |
+
"query": "What is a segment tree?",
|
| 53 |
+
"expected_route": "single_topic",
|
| 54 |
+
"expected_topics": ["Segment Tree"]
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"id": "q10",
|
| 58 |
+
"query": "Explain the sliding window technique",
|
| 59 |
+
"expected_route": "single_topic",
|
| 60 |
+
"expected_topics": ["Sliding Window Technique"]
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"id": "q11",
|
| 64 |
+
"query": "What is the difference between BFS and DFS?",
|
| 65 |
+
"expected_route": "comparison",
|
| 66 |
+
"expected_topics": ["Breadth-First Search", "Depth-First Search"]
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"id": "q12",
|
| 70 |
+
"query": "Compare merge sort vs quick sort",
|
| 71 |
+
"expected_route": "comparison",
|
| 72 |
+
"expected_topics": ["Merge Sort", "Quick Sort"]
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"id": "q13",
|
| 76 |
+
"query": "Difference between a stack and a queue",
|
| 77 |
+
"expected_route": "comparison",
|
| 78 |
+
"expected_topics": ["Stack", "Queue"]
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"id": "q14",
|
| 82 |
+
"query": "Which one is better, Prim's Algorithm or Kruskal's Algorithm, for MST?",
|
| 83 |
+
"expected_route": "comparison",
|
| 84 |
+
"expected_topics": ["Prim's Algorithm (MST)", "Kruskal's Algorithm (MST)"]
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
"id": "q15",
|
| 88 |
+
"query": "Compare AVL Tree and Binary Search Tree",
|
| 89 |
+
"expected_route": "comparison",
|
| 90 |
+
"expected_topics": ["AVL Tree", "Binary Search Tree"]
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"id": "q16",
|
| 94 |
+
"query": "Who won the football World Cup in 2022?",
|
| 95 |
+
"expected_route": "out_of_scope",
|
| 96 |
+
"expected_topics": []
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"id": "q17",
|
| 100 |
+
"query": "What's a good recipe for chocolate cake?",
|
| 101 |
+
"expected_route": "out_of_scope",
|
| 102 |
+
"expected_topics": []
|
| 103 |
+
},
|
| 104 |
+
{
|
| 105 |
+
"id": "q18",
|
| 106 |
+
"query": "What is the capital of France?",
|
| 107 |
+
"expected_route": "out_of_scope",
|
| 108 |
+
"expected_topics": []
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"id": "q19",
|
| 112 |
+
"query": "Can you explain that again?",
|
| 113 |
+
"expected_route": "followup",
|
| 114 |
+
"expected_topics": [],
|
| 115 |
+
"requires_history": true
|
| 116 |
+
},
|
| 117 |
+
{
|
| 118 |
+
"id": "q20",
|
| 119 |
+
"query": "Give me another example",
|
| 120 |
+
"expected_route": "followup",
|
| 121 |
+
"expected_topics": [],
|
| 122 |
+
"requires_history": true
|
| 123 |
+
}
|
| 124 |
+
]
|
history/__init__.py
ADDED
|
File without changes
|
history/models.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Chat history models.
|
| 3 |
+
|
| 4 |
+
SQLite here is used ONLY for exact transcript storage (raw messages,
|
| 5 |
+
timestamps, sessions). It must never store embeddings (that's ChromaDB)
|
| 6 |
+
or user preference/long-term memory (that's mem0).
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
|
| 11 |
+
from auth.db import db
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class ChatSession(db.Model):
|
| 15 |
+
__tablename__ = "chat_sessions"
|
| 16 |
+
|
| 17 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 18 |
+
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False, index=True)
|
| 19 |
+
title = db.Column(db.String(255), nullable=True) # e.g. first user message, truncated
|
| 20 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
| 21 |
+
updated_at = db.Column(
|
| 22 |
+
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
messages = db.relationship(
|
| 26 |
+
"ChatMessage",
|
| 27 |
+
backref="chat_session",
|
| 28 |
+
lazy=True,
|
| 29 |
+
order_by="ChatMessage.created_at",
|
| 30 |
+
cascade="all, delete-orphan",
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
def to_dict(self, include_messages: bool = False) -> dict:
|
| 34 |
+
result = {
|
| 35 |
+
"id": self.id,
|
| 36 |
+
"user_id": self.user_id,
|
| 37 |
+
"title": self.title,
|
| 38 |
+
"created_at": self.created_at.isoformat(),
|
| 39 |
+
"updated_at": self.updated_at.isoformat(),
|
| 40 |
+
}
|
| 41 |
+
if include_messages:
|
| 42 |
+
result["messages"] = [m.to_dict() for m in self.messages]
|
| 43 |
+
return result
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class ChatMessage(db.Model):
|
| 47 |
+
__tablename__ = "chat_messages"
|
| 48 |
+
|
| 49 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 50 |
+
session_id = db.Column(
|
| 51 |
+
db.Integer, db.ForeignKey("chat_sessions.id"), nullable=False, index=True
|
| 52 |
+
)
|
| 53 |
+
role = db.Column(db.String(20), nullable=False) # "user" or "assistant"
|
| 54 |
+
content = db.Column(db.Text, nullable=False)
|
| 55 |
+
|
| 56 |
+
# Optional debugging/eval metadata about how this response was produced.
|
| 57 |
+
route_type = db.Column(db.String(50), nullable=True) # single/comparison/followup/reject
|
| 58 |
+
retrieved_topics = db.Column(db.Text, nullable=True) # JSON-encoded list
|
| 59 |
+
|
| 60 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
| 61 |
+
|
| 62 |
+
def to_dict(self) -> dict:
|
| 63 |
+
return {
|
| 64 |
+
"id": self.id,
|
| 65 |
+
"session_id": self.session_id,
|
| 66 |
+
"role": self.role,
|
| 67 |
+
"content": self.content,
|
| 68 |
+
"route_type": self.route_type,
|
| 69 |
+
"retrieved_topics": self.retrieved_topics,
|
| 70 |
+
"created_at": self.created_at.isoformat(),
|
| 71 |
+
}
|
history/service.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Chat history service.
|
| 3 |
+
|
| 4 |
+
The only module that reads/writes history.models (ChatSession, ChatMessage).
|
| 5 |
+
Callers (chat_service.py, chat_routes.py, the Gradio UI layer) go through
|
| 6 |
+
these functions instead of touching the ORM directly, so persistence
|
| 7 |
+
behavior stays in one place.
|
| 8 |
+
|
| 9 |
+
Reminder of the scope boundary (see history/models.py): SQLite here is for
|
| 10 |
+
exact transcript storage only — no embeddings, no long-term preference
|
| 11 |
+
memory (that's ChromaDB / mem0 respectively).
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import json
|
| 15 |
+
|
| 16 |
+
from auth.db import db
|
| 17 |
+
from history.models import ChatMessage, ChatSession
|
| 18 |
+
from logs.logger import get_logger
|
| 19 |
+
|
| 20 |
+
logger = get_logger(__name__)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def add_message(
|
| 24 |
+
session_id: int,
|
| 25 |
+
role: str,
|
| 26 |
+
content: str,
|
| 27 |
+
route_type: str = None,
|
| 28 |
+
retrieved_topics: list = None,
|
| 29 |
+
) -> ChatMessage:
|
| 30 |
+
"""Append one message (user or assistant) to a session's transcript."""
|
| 31 |
+
message = ChatMessage(
|
| 32 |
+
session_id=session_id,
|
| 33 |
+
role=role,
|
| 34 |
+
content=content,
|
| 35 |
+
route_type=route_type,
|
| 36 |
+
retrieved_topics=json.dumps(retrieved_topics) if retrieved_topics else None,
|
| 37 |
+
)
|
| 38 |
+
db.session.add(message)
|
| 39 |
+
db.session.commit()
|
| 40 |
+
return message
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def get_recent_messages(session_id: int, limit: int = None) -> list:
|
| 44 |
+
"""
|
| 45 |
+
Returns the most recent messages for a session, oldest -> newest, as
|
| 46 |
+
plain dicts ({"role": ..., "content": ...}) — the shape llm/prompts.py
|
| 47 |
+
and the follow-up agent expect.
|
| 48 |
+
"""
|
| 49 |
+
import config
|
| 50 |
+
|
| 51 |
+
limit = limit or config.MAX_RECENT_HISTORY_MESSAGES
|
| 52 |
+
|
| 53 |
+
messages = (
|
| 54 |
+
ChatMessage.query.filter_by(session_id=session_id)
|
| 55 |
+
.order_by(ChatMessage.created_at.desc())
|
| 56 |
+
.limit(limit)
|
| 57 |
+
.all()
|
| 58 |
+
)
|
| 59 |
+
messages.reverse()
|
| 60 |
+
return [{"role": m.role, "content": m.content} for m in messages]
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def list_sessions(user_id: int) -> list:
|
| 64 |
+
"""Returns a summary (no messages) of every chat session for a user,
|
| 65 |
+
most recently updated first — used for the history panel."""
|
| 66 |
+
sessions = (
|
| 67 |
+
ChatSession.query.filter_by(user_id=user_id)
|
| 68 |
+
.order_by(ChatSession.updated_at.desc())
|
| 69 |
+
.all()
|
| 70 |
+
)
|
| 71 |
+
return [s.to_dict(include_messages=False) for s in sessions]
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def get_full_session(session_id: int, user_id: int) -> dict:
|
| 75 |
+
"""Returns the full transcript of one session, or None if it doesn't
|
| 76 |
+
exist or doesn't belong to this user."""
|
| 77 |
+
session_obj = ChatSession.query.filter_by(id=session_id, user_id=user_id).first()
|
| 78 |
+
if session_obj is None:
|
| 79 |
+
return None
|
| 80 |
+
return session_obj.to_dict(include_messages=True)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def delete_session(session_id: int, user_id: int) -> bool:
|
| 84 |
+
"""Deletes a session (and its messages, via cascade) if it belongs to
|
| 85 |
+
this user. Returns True if something was deleted."""
|
| 86 |
+
session_obj = ChatSession.query.filter_by(id=session_id, user_id=user_id).first()
|
| 87 |
+
if session_obj is None:
|
| 88 |
+
return False
|
| 89 |
+
db.session.delete(session_obj)
|
| 90 |
+
db.session.commit()
|
| 91 |
+
return True
|
history/session_manager.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Session resolution helper.
|
| 3 |
+
|
| 4 |
+
Given a logged-in user and an optional client-supplied session_id, decides
|
| 5 |
+
whether to reuse an existing ChatSession or create a new one. Kept separate
|
| 6 |
+
from history/service.py so chat_service.py has one obvious entrypoint for
|
| 7 |
+
"which session am I writing to?" without duplicating that logic per caller.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from auth.db import db
|
| 11 |
+
from history.models import ChatSession
|
| 12 |
+
|
| 13 |
+
TITLE_MAX_LENGTH = 60
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _make_title(first_message: str) -> str:
|
| 17 |
+
first_message = (first_message or "").strip()
|
| 18 |
+
if not first_message:
|
| 19 |
+
return "New chat"
|
| 20 |
+
if len(first_message) <= TITLE_MAX_LENGTH:
|
| 21 |
+
return first_message
|
| 22 |
+
return first_message[: TITLE_MAX_LENGTH - 1].rstrip() + "…"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def resolve_session(user_id: int, session_id: int = None, first_message: str = "") -> ChatSession:
|
| 26 |
+
"""
|
| 27 |
+
Args:
|
| 28 |
+
user_id: the authenticated user's id.
|
| 29 |
+
session_id: an existing session id the client wants to continue.
|
| 30 |
+
Ignored (falls back to creating a new session) if it
|
| 31 |
+
doesn't exist or doesn't belong to this user.
|
| 32 |
+
first_message: used as the auto-generated title when a new session
|
| 33 |
+
is created.
|
| 34 |
+
|
| 35 |
+
Returns:
|
| 36 |
+
The ChatSession to append messages to (already committed, has .id).
|
| 37 |
+
"""
|
| 38 |
+
if session_id is not None:
|
| 39 |
+
existing = ChatSession.query.filter_by(id=session_id, user_id=user_id).first()
|
| 40 |
+
if existing is not None:
|
| 41 |
+
return existing
|
| 42 |
+
|
| 43 |
+
session_obj = ChatSession(user_id=user_id, title=_make_title(first_message))
|
| 44 |
+
db.session.add(session_obj)
|
| 45 |
+
db.session.commit()
|
| 46 |
+
return session_obj
|
llm/__init__.py
ADDED
|
File without changes
|
llm/generate.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Provider factory.
|
| 3 |
+
|
| 4 |
+
This is the ONLY module the rest of the application should import to talk
|
| 5 |
+
to an LLM. It picks the concrete provider based on config.LLM_PROVIDER and
|
| 6 |
+
exposes a single generate(prompt) function. Swapping providers is a
|
| 7 |
+
config change only.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import config
|
| 11 |
+
from logs.logger import get_logger
|
| 12 |
+
|
| 13 |
+
logger = get_logger(__name__)
|
| 14 |
+
|
| 15 |
+
_PROVIDER_SINGLETON = None
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _build_provider():
|
| 19 |
+
provider_name = config.LLM_PROVIDER.lower()
|
| 20 |
+
|
| 21 |
+
if provider_name == "local":
|
| 22 |
+
from llm.local_provider import LocalMistralProvider
|
| 23 |
+
|
| 24 |
+
return LocalMistralProvider()
|
| 25 |
+
|
| 26 |
+
if provider_name == "groq":
|
| 27 |
+
from llm.groq_provider import GroqProvider
|
| 28 |
+
|
| 29 |
+
return GroqProvider()
|
| 30 |
+
|
| 31 |
+
if provider_name == "openai":
|
| 32 |
+
from llm.openai_provider import OpenAIProvider
|
| 33 |
+
|
| 34 |
+
return OpenAIProvider()
|
| 35 |
+
|
| 36 |
+
raise ValueError(
|
| 37 |
+
f"Unknown LLM_PROVIDER={provider_name!r} in config.py. "
|
| 38 |
+
"Expected one of: 'local', 'groq', 'openai'."
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def get_provider():
|
| 43 |
+
"""Module-level singleton so the local model (if used) loads only once."""
|
| 44 |
+
global _PROVIDER_SINGLETON
|
| 45 |
+
if _PROVIDER_SINGLETON is None:
|
| 46 |
+
_PROVIDER_SINGLETON = _build_provider()
|
| 47 |
+
logger.info("LLM provider initialized: %s", _PROVIDER_SINGLETON.name())
|
| 48 |
+
return _PROVIDER_SINGLETON
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def generate(prompt: str, stream: bool = False):
|
| 52 |
+
"""
|
| 53 |
+
The single entrypoint the rest of the app (Flask routes, agents,
|
| 54 |
+
evaluation) should call. Never import a provider class directly outside
|
| 55 |
+
of this module.
|
| 56 |
+
"""
|
| 57 |
+
provider = get_provider()
|
| 58 |
+
return provider.generate(prompt, stream=stream)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def reset_provider():
|
| 62 |
+
"""Mainly for tests: force re-reading config.LLM_PROVIDER on next call."""
|
| 63 |
+
global _PROVIDER_SINGLETON
|
| 64 |
+
_PROVIDER_SINGLETON = None
|
llm/groq_provider.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Groq-hosted LLM provider.
|
| 3 |
+
|
| 4 |
+
Implements the same generate(prompt, stream=False) interface as
|
| 5 |
+
LocalMistralProvider, using Groq's OpenAI-compatible chat completions API.
|
| 6 |
+
Nothing outside llm/generate.py should import this class directly — always
|
| 7 |
+
go through llm.generate.generate().
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import config
|
| 11 |
+
from llm.provider import LLMProvider
|
| 12 |
+
from logs.logger import get_logger
|
| 13 |
+
|
| 14 |
+
logger = get_logger(__name__)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class GroqProvider(LLMProvider):
|
| 18 |
+
def __init__(self):
|
| 19 |
+
if not config.GROQ_API_KEY:
|
| 20 |
+
raise ValueError(
|
| 21 |
+
"GROQ_API_KEY is not set in config/.env. Set it before using "
|
| 22 |
+
"LLM_PROVIDER=groq."
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
from groq import Groq
|
| 26 |
+
|
| 27 |
+
self._client = Groq(api_key=config.GROQ_API_KEY)
|
| 28 |
+
self._model = config.GROQ_MODEL_NAME
|
| 29 |
+
|
| 30 |
+
def generate(self, prompt: str, stream: bool = False):
|
| 31 |
+
"""
|
| 32 |
+
Args:
|
| 33 |
+
prompt: the fully-constructed prompt string from llm/prompts.py.
|
| 34 |
+
stream: if True, returns a generator yielding text chunks as
|
| 35 |
+
Groq streams them back. If False, returns the full
|
| 36 |
+
response as a single string.
|
| 37 |
+
"""
|
| 38 |
+
messages = [{"role": "user", "content": prompt}]
|
| 39 |
+
|
| 40 |
+
if stream:
|
| 41 |
+
return self._generate_stream(messages)
|
| 42 |
+
return self._generate_sync(messages)
|
| 43 |
+
|
| 44 |
+
def _generate_sync(self, messages) -> str:
|
| 45 |
+
try:
|
| 46 |
+
response = self._client.chat.completions.create(
|
| 47 |
+
model=self._model,
|
| 48 |
+
messages=messages,
|
| 49 |
+
temperature=config.LOCAL_MODEL_TEMPERATURE,
|
| 50 |
+
max_tokens=config.LOCAL_MODEL_MAX_NEW_TOKENS,
|
| 51 |
+
stream=False,
|
| 52 |
+
)
|
| 53 |
+
except Exception:
|
| 54 |
+
logger.exception("Groq API call failed (model=%s)", self._model)
|
| 55 |
+
return (
|
| 56 |
+
"Sorry, I couldn't reach the language model service right now. "
|
| 57 |
+
"Please try again in a moment."
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
return response.choices[0].message.content.strip()
|
| 61 |
+
|
| 62 |
+
def _generate_stream(self, messages):
|
| 63 |
+
try:
|
| 64 |
+
stream = self._client.chat.completions.create(
|
| 65 |
+
model=self._model,
|
| 66 |
+
messages=messages,
|
| 67 |
+
temperature=config.LOCAL_MODEL_TEMPERATURE,
|
| 68 |
+
max_tokens=config.LOCAL_MODEL_MAX_NEW_TOKENS,
|
| 69 |
+
stream=True,
|
| 70 |
+
)
|
| 71 |
+
except Exception:
|
| 72 |
+
logger.exception("Groq streaming API call failed (model=%s)", self._model)
|
| 73 |
+
yield (
|
| 74 |
+
"Sorry, I couldn't reach the language model service right now. "
|
| 75 |
+
"Please try again in a moment."
|
| 76 |
+
)
|
| 77 |
+
return
|
| 78 |
+
|
| 79 |
+
for chunk in stream:
|
| 80 |
+
delta = chunk.choices[0].delta.content if chunk.choices else None
|
| 81 |
+
if delta:
|
| 82 |
+
yield delta
|
llm/local_provider.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Local Mistral-7B-Instruct-v0.2 provider.
|
| 3 |
+
|
| 4 |
+
Loads the model lazily (on first generate() call, not at import time) so
|
| 5 |
+
importing this module never requires a GPU or downloaded weights — this
|
| 6 |
+
keeps the rest of the codebase importable/testable without the model
|
| 7 |
+
present.
|
| 8 |
+
|
| 9 |
+
Requires: transformers, bitsandbytes, torch, accelerate (see requirements.txt)
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import config
|
| 13 |
+
from llm.provider import LLMProvider
|
| 14 |
+
from logs.logger import get_logger
|
| 15 |
+
|
| 16 |
+
logger = get_logger(__name__)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class LocalMistralProvider(LLMProvider):
|
| 20 |
+
def __init__(self):
|
| 21 |
+
self._model = None
|
| 22 |
+
self._tokenizer = None
|
| 23 |
+
|
| 24 |
+
def _load(self):
|
| 25 |
+
"""Lazy-load the model + tokenizer on first use."""
|
| 26 |
+
if self._model is not None:
|
| 27 |
+
return
|
| 28 |
+
|
| 29 |
+
logger.info("Loading local model %s ...", config.LOCAL_MODEL_NAME)
|
| 30 |
+
|
| 31 |
+
import torch
|
| 32 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
|
| 33 |
+
|
| 34 |
+
quantization_config = None
|
| 35 |
+
if config.LOCAL_MODEL_LOAD_IN_4BIT:
|
| 36 |
+
quantization_config = BitsAndBytesConfig(
|
| 37 |
+
load_in_4bit=True,
|
| 38 |
+
bnb_4bit_quant_type="nf4",
|
| 39 |
+
bnb_4bit_compute_dtype=torch.float16,
|
| 40 |
+
bnb_4bit_use_double_quant=True,
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
self._tokenizer = AutoTokenizer.from_pretrained(config.LOCAL_MODEL_NAME)
|
| 44 |
+
self._model = AutoModelForCausalLM.from_pretrained(
|
| 45 |
+
config.LOCAL_MODEL_NAME,
|
| 46 |
+
quantization_config=quantization_config,
|
| 47 |
+
device_map="auto",
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
logger.info("Local model loaded.")
|
| 51 |
+
|
| 52 |
+
def generate(self, prompt: str, stream: bool = False):
|
| 53 |
+
self._load()
|
| 54 |
+
|
| 55 |
+
import torch
|
| 56 |
+
|
| 57 |
+
inputs = self._tokenizer(prompt, return_tensors="pt").to(self._model.device)
|
| 58 |
+
|
| 59 |
+
if not stream:
|
| 60 |
+
with torch.no_grad():
|
| 61 |
+
output_ids = self._model.generate(
|
| 62 |
+
**inputs,
|
| 63 |
+
max_new_tokens=config.LOCAL_MODEL_MAX_NEW_TOKENS,
|
| 64 |
+
temperature=config.LOCAL_MODEL_TEMPERATURE,
|
| 65 |
+
do_sample=config.LOCAL_MODEL_TEMPERATURE > 0,
|
| 66 |
+
)
|
| 67 |
+
full_text = self._tokenizer.decode(
|
| 68 |
+
output_ids[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True
|
| 69 |
+
)
|
| 70 |
+
return full_text.strip()
|
| 71 |
+
|
| 72 |
+
return self._generate_streaming(inputs)
|
| 73 |
+
|
| 74 |
+
def _generate_streaming(self, inputs):
|
| 75 |
+
import threading
|
| 76 |
+
|
| 77 |
+
import torch
|
| 78 |
+
from transformers import TextIteratorStreamer
|
| 79 |
+
|
| 80 |
+
streamer = TextIteratorStreamer(
|
| 81 |
+
self._tokenizer, skip_prompt=True, skip_special_tokens=True
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
generation_kwargs = dict(
|
| 85 |
+
**inputs,
|
| 86 |
+
max_new_tokens=config.LOCAL_MODEL_MAX_NEW_TOKENS,
|
| 87 |
+
temperature=config.LOCAL_MODEL_TEMPERATURE,
|
| 88 |
+
do_sample=config.LOCAL_MODEL_TEMPERATURE > 0,
|
| 89 |
+
streamer=streamer,
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
thread = threading.Thread(target=self._model.generate, kwargs=generation_kwargs)
|
| 93 |
+
thread.start()
|
| 94 |
+
|
| 95 |
+
for text_chunk in streamer:
|
| 96 |
+
yield text_chunk
|
| 97 |
+
|
| 98 |
+
thread.join()
|
llm/prompts.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Prompt builders.
|
| 3 |
+
|
| 4 |
+
Every prompt follows this fixed order:
|
| 5 |
+
|
| 6 |
+
System Prompt
|
| 7 |
+
-> Retrieved Context
|
| 8 |
+
-> Relevant mem0 Context
|
| 9 |
+
-> Recent Conversation History
|
| 10 |
+
-> Current User Question
|
| 11 |
+
|
| 12 |
+
One builder per route type, so each route's prompt shape can evolve
|
| 13 |
+
independently:
|
| 14 |
+
|
| 15 |
+
build_single_prompt()
|
| 16 |
+
build_comparison_prompt()
|
| 17 |
+
build_followup_prompt()
|
| 18 |
+
build_direct_prompt()
|
| 19 |
+
build_reject_prompt()
|
| 20 |
+
|
| 21 |
+
These builders only assemble strings — they never call the LLM themselves
|
| 22 |
+
(see llm/generate.py for that).
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
SYSTEM_PROMPT = (
|
| 26 |
+
"You are a patient, precise Data Structures and Algorithms (DSA) tutor. "
|
| 27 |
+
"Answer using ONLY the information provided in the retrieved context below. "
|
| 28 |
+
"If the retrieved context does not contain enough information to answer "
|
| 29 |
+
"confidently, say so explicitly rather than guessing or inventing details. "
|
| 30 |
+
"Explain concepts clearly, use the time/space complexity and examples given "
|
| 31 |
+
"when relevant, and adapt your explanation style to any user preferences "
|
| 32 |
+
"provided in the memory context."
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
# ---------------------------------------------------------------------------
|
| 37 |
+
# Shared formatting helpers
|
| 38 |
+
# ---------------------------------------------------------------------------
|
| 39 |
+
|
| 40 |
+
def _format_chunks(chunks: list, heading: str = None) -> str:
|
| 41 |
+
if not chunks:
|
| 42 |
+
return ""
|
| 43 |
+
lines = []
|
| 44 |
+
if heading:
|
| 45 |
+
lines.append(f"### {heading}")
|
| 46 |
+
for chunk in chunks:
|
| 47 |
+
meta = chunk.get("metadata", {})
|
| 48 |
+
section = meta.get("section", "")
|
| 49 |
+
topic = meta.get("topic", "")
|
| 50 |
+
lines.append(f"[{topic} - {section}]\n{chunk.get('text', '')}")
|
| 51 |
+
return "\n\n".join(lines)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _format_mem0_context(mem0_context) -> str:
|
| 55 |
+
if not mem0_context:
|
| 56 |
+
return ""
|
| 57 |
+
if isinstance(mem0_context, str):
|
| 58 |
+
body = mem0_context
|
| 59 |
+
else:
|
| 60 |
+
# list of preference strings/facts returned by memory/mem0_client.py
|
| 61 |
+
body = "\n".join(f"- {item}" for item in mem0_context)
|
| 62 |
+
return f"### User Preferences / Memory\n{body}"
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _format_history(recent_messages: list) -> str:
|
| 66 |
+
if not recent_messages:
|
| 67 |
+
return ""
|
| 68 |
+
lines = ["### Recent Conversation"]
|
| 69 |
+
for message in recent_messages:
|
| 70 |
+
role = message.get("role", "user").capitalize()
|
| 71 |
+
content = message.get("content", "")
|
| 72 |
+
lines.append(f"{role}: {content}")
|
| 73 |
+
return "\n".join(lines)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _assemble(*sections: str) -> str:
|
| 77 |
+
"""Joins non-empty sections with blank lines, preserving fixed order."""
|
| 78 |
+
return "\n\n".join(s for s in sections if s)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# ---------------------------------------------------------------------------
|
| 82 |
+
# Per-route builders
|
| 83 |
+
# ---------------------------------------------------------------------------
|
| 84 |
+
|
| 85 |
+
def build_single_prompt(
|
| 86 |
+
chunks: list,
|
| 87 |
+
mem0_context=None,
|
| 88 |
+
recent_history: list = None,
|
| 89 |
+
query: str = "",
|
| 90 |
+
) -> str:
|
| 91 |
+
"""Single-topic route, e.g. 'Explain Merge Sort'."""
|
| 92 |
+
retrieved = _format_chunks(chunks, heading="Retrieved Context")
|
| 93 |
+
mem0_section = _format_mem0_context(mem0_context)
|
| 94 |
+
history_section = _format_history(recent_history)
|
| 95 |
+
question_section = f"### Current Question\n{query}"
|
| 96 |
+
|
| 97 |
+
return _assemble(
|
| 98 |
+
SYSTEM_PROMPT, retrieved, mem0_section, history_section, question_section
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def build_comparison_prompt(
|
| 103 |
+
comparison_context: dict,
|
| 104 |
+
mem0_context=None,
|
| 105 |
+
recent_history: list = None,
|
| 106 |
+
query: str = "",
|
| 107 |
+
) -> str:
|
| 108 |
+
"""Comparison route, e.g. 'Difference between BFS and DFS'."""
|
| 109 |
+
retrieved_sections = []
|
| 110 |
+
for topic, chunks in comparison_context.items():
|
| 111 |
+
retrieved_sections.append(_format_chunks(chunks, heading=f"Retrieved Context: {topic}"))
|
| 112 |
+
retrieved = "\n\n".join(s for s in retrieved_sections if s)
|
| 113 |
+
|
| 114 |
+
mem0_section = _format_mem0_context(mem0_context)
|
| 115 |
+
history_section = _format_history(recent_history)
|
| 116 |
+
question_section = f"### Current Question\n{query}"
|
| 117 |
+
|
| 118 |
+
instruction = (
|
| 119 |
+
"Compare the topics above directly: highlight the key differences in "
|
| 120 |
+
"definition, time/space complexity, and when to use each."
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
return _assemble(
|
| 124 |
+
SYSTEM_PROMPT, instruction, retrieved, mem0_section, history_section, question_section
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def build_followup_prompt(
|
| 129 |
+
followup_context: dict,
|
| 130 |
+
mem0_context=None,
|
| 131 |
+
recent_history: list = None,
|
| 132 |
+
query: str = "",
|
| 133 |
+
) -> str:
|
| 134 |
+
"""
|
| 135 |
+
Follow-up route, e.g. 'Can you explain that again?'. Reuses recent
|
| 136 |
+
conversation as primary context; only includes `additional_chunks` if
|
| 137 |
+
the follow-up agent decided a fresh retrieval was necessary.
|
| 138 |
+
"""
|
| 139 |
+
additional_chunks = followup_context.get("additional_chunks", [])
|
| 140 |
+
retrieved = _format_chunks(additional_chunks, heading="Additional Retrieved Context")
|
| 141 |
+
|
| 142 |
+
mem0_section = _format_mem0_context(mem0_context)
|
| 143 |
+
# Prefer the followup_context's own recent_messages if the caller didn't
|
| 144 |
+
# pass a separate recent_history override.
|
| 145 |
+
history_section = _format_history(
|
| 146 |
+
recent_history or followup_context.get("recent_messages", [])
|
| 147 |
+
)
|
| 148 |
+
question_section = f"### Current Question\n{query}"
|
| 149 |
+
|
| 150 |
+
instruction = (
|
| 151 |
+
"This is a follow-up question. Use the recent conversation above to "
|
| 152 |
+
"understand what the user is referring to before answering."
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
WORKED_EXAMPLE_INSTRUCTION = (
|
| 156 |
+
"If the user provides their own concrete data (a list, array, or set of "
|
| 157 |
+
"numbers) and asks you to apply a search/sort/algorithm to it, walk "
|
| 158 |
+
"through the exact steps of the algorithm from the retrieved context "
|
| 159 |
+
"on their data. Do not substitute a different algorithm than the one "
|
| 160 |
+
"being discussed, even if their data doesn't perfectly fit it (e.g. "
|
| 161 |
+
"note if the list isn't sorted, but still apply the same algorithm's "
|
| 162 |
+
"logic unless the user explicitly asks for a different one)."
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
return _assemble(
|
| 166 |
+
SYSTEM_PROMPT, instruction, WORKED_EXAMPLE_INSTRUCTION, retrieved, mem0_section, history_section, question_section,
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def build_direct_prompt(
|
| 171 |
+
mem0_context=None,
|
| 172 |
+
recent_history: list = None,
|
| 173 |
+
query: str = "",
|
| 174 |
+
) -> str:
|
| 175 |
+
"""
|
| 176 |
+
Fallback builder for cases with no retrieved KB context at all (e.g. a
|
| 177 |
+
general greeting, or a follow-up so light it needs no context beyond
|
| 178 |
+
conversation history). Still follows the same section ordering, simply
|
| 179 |
+
omitting the "Retrieved Context" section.
|
| 180 |
+
"""
|
| 181 |
+
mem0_section = _format_mem0_context(mem0_context)
|
| 182 |
+
history_section = _format_history(recent_history)
|
| 183 |
+
question_section = f"### Current Question\n{query}"
|
| 184 |
+
|
| 185 |
+
return _assemble(SYSTEM_PROMPT, mem0_section, history_section, question_section)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def build_reject_prompt(query: str = "") -> str:
|
| 189 |
+
"""
|
| 190 |
+
Out-of-scope route, e.g. 'Who won the World Cup?'. Kept deliberately
|
| 191 |
+
simple/static — no retrieved context, no memory, no history — since the
|
| 192 |
+
goal is just a polite, on-brand refusal, not a reasoned answer.
|
| 193 |
+
"""
|
| 194 |
+
instruction = (
|
| 195 |
+
"The user's question is outside the scope of this DSA tutor "
|
| 196 |
+
"(it isn't a data structures / algorithms question, or the knowledge "
|
| 197 |
+
"base has no relevant information). Politely explain that you can "
|
| 198 |
+
"only help with Data Structures and Algorithms topics, and invite "
|
| 199 |
+
"them to ask a DSA-related question instead. Keep it brief and friendly."
|
| 200 |
+
)
|
| 201 |
+
question_section = f"### User's Message\n{query}"
|
| 202 |
+
|
| 203 |
+
return _assemble(SYSTEM_PROMPT, instruction, question_section)
|
llm/provider.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
LLM provider interface.
|
| 3 |
+
|
| 4 |
+
The rest of the application (router, chat endpoints, evaluation) must
|
| 5 |
+
NEVER call a specific model directly. Everything goes through
|
| 6 |
+
generate(prompt) on whatever provider is configured via
|
| 7 |
+
config.LLM_PROVIDER. Switching from local Mistral to Groq or OpenAI later
|
| 8 |
+
should require changing config only — no changes to callers.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from abc import ABC, abstractmethod
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class LLMProvider(ABC):
|
| 15 |
+
"""Base interface every provider (local, groq, openai, ...) must implement."""
|
| 16 |
+
|
| 17 |
+
@abstractmethod
|
| 18 |
+
def generate(self, prompt: str, stream: bool = False):
|
| 19 |
+
"""
|
| 20 |
+
Args:
|
| 21 |
+
prompt: the fully-constructed prompt string (see llm/prompts.py).
|
| 22 |
+
stream: if True, returns a generator yielding text chunks as
|
| 23 |
+
they're produced, instead of a single string. Providers
|
| 24 |
+
that can't stream should fall back to yielding the full
|
| 25 |
+
response once.
|
| 26 |
+
|
| 27 |
+
Returns:
|
| 28 |
+
str if stream=False, otherwise a generator[str].
|
| 29 |
+
"""
|
| 30 |
+
raise NotImplementedError
|
| 31 |
+
|
| 32 |
+
def name(self) -> str:
|
| 33 |
+
return self.__class__.__name__
|
logs/__init__.py
ADDED
|
File without changes
|
logs/app.log
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
2026-08-02 13:43:39,527 | INFO | dsa_chatbot.__main__ | Loading embedding model: all-MiniLM-L6-v2
|
| 2 |
+
2026-08-02 13:44:07,549 | WARNING | dsa_chatbot.__main__ | The following KB files have no topic_metadata.json entry and will be skipped: ['01_knapsack.txt', 'activity_selection_problem.txt', 'backtracking_overview.txt', 'bit_manipulation_basics.txt', 'coin_change_problem.txt', 'dijkstras_algorithm.txt', 'dynamic_programming_overview.txt', 'edit_distance.txt', 'fractional_knapsack.txt', 'greedy_algorithms_overview.txt', 'hash_set_vs_hash_map.txt', 'hash_table.txt', 'kruskals_algorithm_mst.txt', 'longest_common_subsequence.txt', 'longest_increasing_subsequence.txt', 'n-queens_problem.txt', 'prims_algorithm_mst.txt', 'recursion_overview.txt', 'subset_sum_power_set_generation.txt', 'xor_tricks.txt']
|
| 3 |
+
2026-08-02 13:44:08,102 | INFO | dsa_chatbot.__main__ | Built 98 chunks from 54 knowledge-base files.
|
| 4 |
+
2026-08-02 13:44:08,102 | INFO | dsa_chatbot.__main__ | Embedding 98 chunks...
|
| 5 |
+
2026-08-02 13:44:13,562 | INFO | dsa_chatbot.__main__ | Index build complete. Collection 'dsa_knowledge_base' now has 98 chunks.
|
| 6 |
+
2026-08-02 13:45:19,505 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'Explain how binary search works' | topic=Binary Search
|
| 7 |
+
2026-08-02 13:45:19,648 | INFO | dsa_chatbot.rag.retriever | Query='Explain how binary search works' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 8 |
+
2026-08-02 13:45:19,658 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is the time complexity of merge sort?' | topic=Insertion Sort
|
| 9 |
+
2026-08-02 13:45:19,680 | INFO | dsa_chatbot.rag.retriever | Query='What is the time complexity of merge sort?' | candidates=1 | passed_threshold=0 | threshold=0.45
|
| 10 |
+
2026-08-02 13:45:19,681 | WARNING | dsa_chatbot.agents.router | SINGLE_TOPIC query had no chunks pass the similarity threshold: 'What is the time complexity of merge sort?'
|
| 11 |
+
2026-08-02 13:45:19,688 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'Explain quick sort with an example' | topic=Bucket Sort
|
| 12 |
+
2026-08-02 13:45:19,718 | INFO | dsa_chatbot.rag.retriever | Query='Explain quick sort with an example' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 13 |
+
2026-08-02 13:45:19,727 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is a Trie used for?' | topic=Insertion Sort
|
| 14 |
+
2026-08-02 13:45:19,752 | INFO | dsa_chatbot.rag.retriever | Query='What is a Trie used for?' | candidates=1 | passed_threshold=0 | threshold=0.45
|
| 15 |
+
2026-08-02 13:45:19,753 | WARNING | dsa_chatbot.agents.router | SINGLE_TOPIC query had no chunks pass the similarity threshold: 'What is a Trie used for?'
|
| 16 |
+
2026-08-02 13:45:19,763 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is a doubly linked list?' | topic=Doubly Linked List
|
| 17 |
+
2026-08-02 13:45:19,791 | INFO | dsa_chatbot.rag.retriever | Query='What is a doubly linked list?' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 18 |
+
2026-08-02 13:45:19,796 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'Explain how a stack works' | topic=Stack
|
| 19 |
+
2026-08-02 13:45:19,817 | INFO | dsa_chatbot.rag.retriever | Query='Explain how a stack works' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 20 |
+
2026-08-02 13:45:19,825 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is topological sort used for?' | topic=Insertion Sort
|
| 21 |
+
2026-08-02 13:45:19,848 | INFO | dsa_chatbot.rag.retriever | Query='What is topological sort used for?' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 22 |
+
2026-08-02 13:45:19,852 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: "Explain Dijkstra's Algorithm" | topic=Dijkstra's Algorithm
|
| 23 |
+
2026-08-02 13:45:19,899 | INFO | dsa_chatbot.rag.retriever | Query="Explain Dijkstra's Algorithm" | candidates=0 | passed_threshold=0 | threshold=0.45
|
| 24 |
+
2026-08-02 13:45:19,899 | WARNING | dsa_chatbot.agents.router | SINGLE_TOPIC query had no chunks pass the similarity threshold: "Explain Dijkstra's Algorithm"
|
| 25 |
+
2026-08-02 13:45:19,905 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is a segment tree?' | topic=Insertion Sort
|
| 26 |
+
2026-08-02 13:45:19,929 | INFO | dsa_chatbot.rag.retriever | Query='What is a segment tree?' | candidates=1 | passed_threshold=0 | threshold=0.45
|
| 27 |
+
2026-08-02 13:45:19,929 | WARNING | dsa_chatbot.agents.router | SINGLE_TOPIC query had no chunks pass the similarity threshold: 'What is a segment tree?'
|
| 28 |
+
2026-08-02 13:45:19,935 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'Explain the sliding window technique' | topic=Sliding Window Technique
|
| 29 |
+
2026-08-02 13:45:19,958 | INFO | dsa_chatbot.rag.retriever | Query='Explain the sliding window technique' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 30 |
+
2026-08-02 13:45:19,966 | INFO | dsa_chatbot.agents.router | Routed to COMPARISON: 'What is the difference between BFS and DFS?' | topics=['Breadth-First Search', 'Depth-First Search', 'Insertion Sort']
|
| 31 |
+
2026-08-02 13:45:19,995 | INFO | dsa_chatbot.rag.retriever | Query='Breadth-First Search' | candidates=5 | passed_threshold=5 | threshold=0.45
|
| 32 |
+
2026-08-02 13:45:20,013 | INFO | dsa_chatbot.rag.retriever | Query='Depth-First Search' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 33 |
+
2026-08-02 13:45:20,033 | INFO | dsa_chatbot.rag.retriever | Query='Insertion Sort' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 34 |
+
2026-08-02 13:45:20,042 | INFO | dsa_chatbot.agents.router | Routed to COMPARISON: 'Compare merge sort vs quick sort' | topics=['Bucket Sort', 'Merge Sort', 'Quick Sort']
|
| 35 |
+
2026-08-02 13:45:20,062 | INFO | dsa_chatbot.rag.retriever | Query='Bucket Sort' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 36 |
+
2026-08-02 13:45:20,087 | INFO | dsa_chatbot.rag.retriever | Query='Merge Sort' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 37 |
+
2026-08-02 13:45:20,107 | INFO | dsa_chatbot.rag.retriever | Query='Quick Sort' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 38 |
+
2026-08-02 13:45:20,115 | INFO | dsa_chatbot.agents.router | Routed to COMPARISON: 'Difference between a stack and a queue' | topics=['Queue', 'Stack']
|
| 39 |
+
2026-08-02 13:45:20,168 | INFO | dsa_chatbot.rag.retriever | Query='Queue' | candidates=5 | passed_threshold=5 | threshold=0.45
|
| 40 |
+
2026-08-02 13:45:20,190 | INFO | dsa_chatbot.rag.retriever | Query='Stack' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 41 |
+
2026-08-02 13:45:20,215 | INFO | dsa_chatbot.agents.router | Routed to COMPARISON: "Which one is better, Prim's Algorithm or Kruskal's Algorithm, for MST?" | topics=["Dijkstra's Algorithm", 'Insertion Sort', "Kruskal's Algorithm (MST)", "Prim's Algorithm (MST)"]
|
| 42 |
+
2026-08-02 13:45:20,245 | INFO | dsa_chatbot.rag.retriever | Query="Dijkstra's Algorithm" | candidates=0 | passed_threshold=0 | threshold=0.45
|
| 43 |
+
2026-08-02 13:45:20,271 | INFO | dsa_chatbot.rag.retriever | Query='Insertion Sort' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 44 |
+
2026-08-02 13:45:20,305 | INFO | dsa_chatbot.rag.retriever | Query="Kruskal's Algorithm (MST)" | candidates=0 | passed_threshold=0 | threshold=0.45
|
| 45 |
+
2026-08-02 13:45:20,330 | INFO | dsa_chatbot.rag.retriever | Query="Prim's Algorithm (MST)" | candidates=0 | passed_threshold=0 | threshold=0.45
|
| 46 |
+
2026-08-02 13:45:20,330 | WARNING | dsa_chatbot.agents.comparison | Comparison agent: no chunks passed the similarity threshold for topic "Dijkstra's Algorithm"
|
| 47 |
+
2026-08-02 13:45:20,330 | WARNING | dsa_chatbot.agents.comparison | Comparison agent: no chunks passed the similarity threshold for topic "Kruskal's Algorithm (MST)"
|
| 48 |
+
2026-08-02 13:45:20,330 | WARNING | dsa_chatbot.agents.comparison | Comparison agent: no chunks passed the similarity threshold for topic "Prim's Algorithm (MST)"
|
| 49 |
+
2026-08-02 13:45:20,331 | WARNING | dsa_chatbot.agents.router | COMPARISON query missing sufficient context for one or more topics: ["Dijkstra's Algorithm", 'Insertion Sort', "Kruskal's Algorithm (MST)", "Prim's Algorithm (MST)"]
|
| 50 |
+
2026-08-02 13:45:20,341 | INFO | dsa_chatbot.agents.router | Routed to COMPARISON: 'Compare AVL Tree and Binary Search Tree' | topics=['AVL Tree', 'Binary Search', 'Binary Search Tree', 'Linear Search', 'Trie']
|
| 51 |
+
2026-08-02 13:45:20,362 | INFO | dsa_chatbot.rag.retriever | Query='AVL Tree' | candidates=5 | passed_threshold=5 | threshold=0.45
|
| 52 |
+
2026-08-02 13:45:20,380 | INFO | dsa_chatbot.rag.retriever | Query='Binary Search' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 53 |
+
2026-08-02 13:45:20,403 | INFO | dsa_chatbot.rag.retriever | Query='Binary Search Tree' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 54 |
+
2026-08-02 13:45:20,426 | INFO | dsa_chatbot.rag.retriever | Query='Linear Search' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 55 |
+
2026-08-02 13:45:20,448 | INFO | dsa_chatbot.rag.retriever | Query='Trie' | candidates=5 | passed_threshold=4 | threshold=0.45
|
| 56 |
+
2026-08-02 13:45:20,459 | INFO | dsa_chatbot.agents.router | Routed to OUT_OF_SCOPE: 'Who won the football World Cup in 2022?'
|
| 57 |
+
2026-08-02 13:45:20,466 | INFO | dsa_chatbot.agents.router | Routed to OUT_OF_SCOPE: "What's a good recipe for chocolate cake?"
|
| 58 |
+
2026-08-02 13:45:20,474 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is the capital of France?' | topic=Insertion Sort
|
| 59 |
+
2026-08-02 13:45:20,495 | INFO | dsa_chatbot.rag.retriever | Query='What is the capital of France?' | candidates=1 | passed_threshold=0 | threshold=0.45
|
| 60 |
+
2026-08-02 13:45:20,495 | WARNING | dsa_chatbot.agents.router | SINGLE_TOPIC query had no chunks pass the similarity threshold: 'What is the capital of France?'
|
| 61 |
+
2026-08-02 13:45:20,499 | INFO | dsa_chatbot.agents.router | Routed to FOLLOWUP: 'Can you explain that again?'
|
| 62 |
+
2026-08-02 13:45:20,517 | INFO | dsa_chatbot.agents.followup | Follow-up query reuses existing conversation context (topics=['Merge Sort', 'Insertion Sort']); skipping fresh retrieval.
|
| 63 |
+
2026-08-02 13:45:20,530 | INFO | dsa_chatbot.agents.router | Routed to OUT_OF_SCOPE: 'Give me another example'
|
| 64 |
+
2026-08-02 13:45:20,531 | INFO | dsa_chatbot.__main__ | Evaluation complete: {'total_questions': 20, 'route_accuracy': 0.9, 'avg_topic_recall': 0.65, 'retrieval_latency_ms_avg': 51.74, 'retrieval_latency_ms_p95': 140.89}
|
| 65 |
+
2026-08-02 13:59:25,086 | INFO | dsa_chatbot.__main__ | Loading embedding model: all-MiniLM-L6-v2
|
| 66 |
+
2026-08-02 13:59:31,186 | INFO | dsa_chatbot.__main__ | Existing collection 'dsa_knowledge_base' deleted.
|
| 67 |
+
2026-08-02 13:59:31,554 | INFO | dsa_chatbot.__main__ | Built 150 chunks from 54 knowledge-base files.
|
| 68 |
+
2026-08-02 13:59:31,555 | INFO | dsa_chatbot.__main__ | Embedding 150 chunks...
|
| 69 |
+
2026-08-02 13:59:39,737 | INFO | dsa_chatbot.__main__ | Index build complete. Collection 'dsa_knowledge_base' now has 150 chunks.
|
| 70 |
+
2026-08-02 14:00:37,066 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'Explain how binary search works' | topic=Binary Search
|
| 71 |
+
2026-08-02 14:00:37,234 | INFO | dsa_chatbot.rag.retriever | Query='Explain how binary search works' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 72 |
+
2026-08-02 14:00:37,248 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is the time complexity of merge sort?' | topic=Insertion Sort
|
| 73 |
+
2026-08-02 14:00:37,273 | INFO | dsa_chatbot.rag.retriever | Query='What is the time complexity of merge sort?' | candidates=1 | passed_threshold=0 | threshold=0.45
|
| 74 |
+
2026-08-02 14:00:37,274 | WARNING | dsa_chatbot.agents.router | SINGLE_TOPIC query had no chunks pass the similarity threshold: 'What is the time complexity of merge sort?'
|
| 75 |
+
2026-08-02 14:00:37,288 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'Explain quick sort with an example' | topic=Bucket Sort
|
| 76 |
+
2026-08-02 14:00:37,310 | INFO | dsa_chatbot.rag.retriever | Query='Explain quick sort with an example' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 77 |
+
2026-08-02 14:00:37,321 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is a Trie used for?' | topic=Insertion Sort
|
| 78 |
+
2026-08-02 14:00:37,345 | INFO | dsa_chatbot.rag.retriever | Query='What is a Trie used for?' | candidates=1 | passed_threshold=0 | threshold=0.45
|
| 79 |
+
2026-08-02 14:00:37,346 | WARNING | dsa_chatbot.agents.router | SINGLE_TOPIC query had no chunks pass the similarity threshold: 'What is a Trie used for?'
|
| 80 |
+
2026-08-02 14:00:37,357 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is a doubly linked list?' | topic=Doubly Linked List
|
| 81 |
+
2026-08-02 14:00:37,378 | INFO | dsa_chatbot.rag.retriever | Query='What is a doubly linked list?' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 82 |
+
2026-08-02 14:00:37,386 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'Explain how a stack works' | topic=Stack
|
| 83 |
+
2026-08-02 14:00:37,405 | INFO | dsa_chatbot.rag.retriever | Query='Explain how a stack works' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 84 |
+
2026-08-02 14:00:37,415 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is topological sort used for?' | topic=Insertion Sort
|
| 85 |
+
2026-08-02 14:00:37,433 | INFO | dsa_chatbot.rag.retriever | Query='What is topological sort used for?' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 86 |
+
2026-08-02 14:00:37,439 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: "Explain Dijkstra's Algorithm" | topic=Dijkstra's Algorithm
|
| 87 |
+
2026-08-02 14:00:37,463 | INFO | dsa_chatbot.rag.retriever | Query="Explain Dijkstra's Algorithm" | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 88 |
+
2026-08-02 14:00:37,471 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is a segment tree?' | topic=Insertion Sort
|
| 89 |
+
2026-08-02 14:00:37,493 | INFO | dsa_chatbot.rag.retriever | Query='What is a segment tree?' | candidates=1 | passed_threshold=0 | threshold=0.45
|
| 90 |
+
2026-08-02 14:00:37,494 | WARNING | dsa_chatbot.agents.router | SINGLE_TOPIC query had no chunks pass the similarity threshold: 'What is a segment tree?'
|
| 91 |
+
2026-08-02 14:00:37,501 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'Explain the sliding window technique' | topic=Sliding Window Technique
|
| 92 |
+
2026-08-02 14:00:37,521 | INFO | dsa_chatbot.rag.retriever | Query='Explain the sliding window technique' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 93 |
+
2026-08-02 14:00:37,539 | INFO | dsa_chatbot.agents.router | Routed to COMPARISON: 'What is the difference between BFS and DFS?' | topics=['Breadth-First Search', 'Depth-First Search', 'Insertion Sort']
|
| 94 |
+
2026-08-02 14:00:37,597 | INFO | dsa_chatbot.rag.retriever | Query='Breadth-First Search' | candidates=5 | passed_threshold=5 | threshold=0.45
|
| 95 |
+
2026-08-02 14:00:37,624 | INFO | dsa_chatbot.rag.retriever | Query='Depth-First Search' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 96 |
+
2026-08-02 14:00:37,647 | INFO | dsa_chatbot.rag.retriever | Query='Insertion Sort' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 97 |
+
2026-08-02 14:00:37,666 | INFO | dsa_chatbot.agents.router | Routed to COMPARISON: 'Compare merge sort vs quick sort' | topics=['Bucket Sort', 'Merge Sort', 'Quick Sort']
|
| 98 |
+
2026-08-02 14:00:37,699 | INFO | dsa_chatbot.rag.retriever | Query='Bucket Sort' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 99 |
+
2026-08-02 14:00:37,725 | INFO | dsa_chatbot.rag.retriever | Query='Merge Sort' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 100 |
+
2026-08-02 14:00:37,751 | INFO | dsa_chatbot.rag.retriever | Query='Quick Sort' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 101 |
+
2026-08-02 14:00:37,767 | INFO | dsa_chatbot.agents.router | Routed to COMPARISON: 'Difference between a stack and a queue' | topics=['Queue', 'Stack']
|
| 102 |
+
2026-08-02 14:00:37,821 | INFO | dsa_chatbot.rag.retriever | Query='Queue' | candidates=5 | passed_threshold=5 | threshold=0.45
|
| 103 |
+
2026-08-02 14:00:37,851 | INFO | dsa_chatbot.rag.retriever | Query='Stack' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 104 |
+
2026-08-02 14:00:37,892 | INFO | dsa_chatbot.agents.router | Routed to COMPARISON: "Which one is better, Prim's Algorithm or Kruskal's Algorithm, for MST?" | topics=["Dijkstra's Algorithm", 'Insertion Sort', "Kruskal's Algorithm (MST)", "Prim's Algorithm (MST)"]
|
| 105 |
+
2026-08-02 14:00:37,919 | INFO | dsa_chatbot.rag.retriever | Query="Dijkstra's Algorithm" | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 106 |
+
2026-08-02 14:00:37,944 | INFO | dsa_chatbot.rag.retriever | Query='Insertion Sort' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 107 |
+
2026-08-02 14:00:37,977 | INFO | dsa_chatbot.rag.retriever | Query="Kruskal's Algorithm (MST)" | candidates=5 | passed_threshold=5 | threshold=0.45
|
| 108 |
+
2026-08-02 14:00:38,005 | INFO | dsa_chatbot.rag.retriever | Query="Prim's Algorithm (MST)" | candidates=5 | passed_threshold=5 | threshold=0.45
|
| 109 |
+
2026-08-02 14:00:38,022 | INFO | dsa_chatbot.agents.router | Routed to COMPARISON: 'Compare AVL Tree and Binary Search Tree' | topics=['AVL Tree', 'Binary Search', 'Binary Search Tree', 'Linear Search', 'Trie']
|
| 110 |
+
2026-08-02 14:00:38,043 | INFO | dsa_chatbot.rag.retriever | Query='AVL Tree' | candidates=5 | passed_threshold=5 | threshold=0.45
|
| 111 |
+
2026-08-02 14:00:38,058 | INFO | dsa_chatbot.rag.retriever | Query='Binary Search' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 112 |
+
2026-08-02 14:00:38,081 | INFO | dsa_chatbot.rag.retriever | Query='Binary Search Tree' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 113 |
+
2026-08-02 14:00:38,109 | INFO | dsa_chatbot.rag.retriever | Query='Linear Search' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 114 |
+
2026-08-02 14:00:38,126 | INFO | dsa_chatbot.rag.retriever | Query='Trie' | candidates=5 | passed_threshold=4 | threshold=0.45
|
| 115 |
+
2026-08-02 14:00:38,138 | INFO | dsa_chatbot.agents.router | Routed to OUT_OF_SCOPE: 'Who won the football World Cup in 2022?'
|
| 116 |
+
2026-08-02 14:00:38,152 | INFO | dsa_chatbot.agents.router | Routed to OUT_OF_SCOPE: "What's a good recipe for chocolate cake?"
|
| 117 |
+
2026-08-02 14:00:38,164 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is the capital of France?' | topic=Insertion Sort
|
| 118 |
+
2026-08-02 14:00:38,186 | INFO | dsa_chatbot.rag.retriever | Query='What is the capital of France?' | candidates=1 | passed_threshold=0 | threshold=0.45
|
| 119 |
+
2026-08-02 14:00:38,186 | WARNING | dsa_chatbot.agents.router | SINGLE_TOPIC query had no chunks pass the similarity threshold: 'What is the capital of France?'
|
| 120 |
+
2026-08-02 14:00:38,195 | INFO | dsa_chatbot.agents.router | Routed to FOLLOWUP: 'Can you explain that again?'
|
| 121 |
+
2026-08-02 14:00:38,226 | INFO | dsa_chatbot.agents.followup | Follow-up query reuses existing conversation context (topics=['Merge Sort', 'Insertion Sort']); skipping fresh retrieval.
|
| 122 |
+
2026-08-02 14:00:38,236 | INFO | dsa_chatbot.agents.router | Routed to OUT_OF_SCOPE: 'Give me another example'
|
| 123 |
+
2026-08-02 14:00:38,237 | INFO | dsa_chatbot.__main__ | Evaluation complete: {'total_questions': 20, 'route_accuracy': 0.9, 'avg_topic_recall': 0.75, 'retrieval_latency_ms_avg': 59.26, 'retrieval_latency_ms_p95': 155.61}
|
| 124 |
+
2026-08-02 14:03:52,099 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'Explain how binary search works' | topic=Binary Search
|
| 125 |
+
2026-08-02 14:03:52,274 | INFO | dsa_chatbot.rag.retriever | Query='Explain how binary search works' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 126 |
+
2026-08-02 14:03:52,288 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is the time complexity of merge sort?' | topic=Merge Sort
|
| 127 |
+
2026-08-02 14:03:52,311 | INFO | dsa_chatbot.rag.retriever | Query='What is the time complexity of merge sort?' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 128 |
+
2026-08-02 14:03:52,324 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'Explain quick sort with an example' | topic=Quick Sort
|
| 129 |
+
2026-08-02 14:03:52,350 | INFO | dsa_chatbot.rag.retriever | Query='Explain quick sort with an example' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 130 |
+
2026-08-02 14:03:52,362 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is a Trie used for?' | topic=Trie
|
| 131 |
+
2026-08-02 14:03:52,384 | INFO | dsa_chatbot.rag.retriever | Query='What is a Trie used for?' | candidates=5 | passed_threshold=4 | threshold=0.45
|
| 132 |
+
2026-08-02 14:03:52,398 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is a doubly linked list?' | topic=Doubly Linked List
|
| 133 |
+
2026-08-02 14:03:52,419 | INFO | dsa_chatbot.rag.retriever | Query='What is a doubly linked list?' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 134 |
+
2026-08-02 14:03:52,430 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'Explain how a stack works' | topic=Stack
|
| 135 |
+
2026-08-02 14:03:52,450 | INFO | dsa_chatbot.rag.retriever | Query='Explain how a stack works' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 136 |
+
2026-08-02 14:03:52,462 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is topological sort used for?' | topic=Topological Sort
|
| 137 |
+
2026-08-02 14:03:52,483 | INFO | dsa_chatbot.rag.retriever | Query='What is topological sort used for?' | candidates=5 | passed_threshold=5 | threshold=0.45
|
| 138 |
+
2026-08-02 14:03:52,490 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: "Explain Dijkstra's Algorithm" | topic=Dijkstra's Algorithm
|
| 139 |
+
2026-08-02 14:03:52,516 | INFO | dsa_chatbot.rag.retriever | Query="Explain Dijkstra's Algorithm" | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 140 |
+
2026-08-02 14:03:52,522 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is a segment tree?' | topic=Segment Tree
|
| 141 |
+
2026-08-02 14:03:52,547 | INFO | dsa_chatbot.rag.retriever | Query='What is a segment tree?' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 142 |
+
2026-08-02 14:03:52,561 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'Explain the sliding window technique' | topic=Sliding Window Technique
|
| 143 |
+
2026-08-02 14:03:52,582 | INFO | dsa_chatbot.rag.retriever | Query='Explain the sliding window technique' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 144 |
+
2026-08-02 14:03:52,605 | INFO | dsa_chatbot.agents.router | Routed to COMPARISON: 'What is the difference between BFS and DFS?' | topics=['Breadth-First Search', 'Depth-First Search']
|
| 145 |
+
2026-08-02 14:03:52,627 | INFO | dsa_chatbot.rag.retriever | Query='Breadth-First Search' | candidates=5 | passed_threshold=5 | threshold=0.45
|
| 146 |
+
2026-08-02 14:03:52,649 | INFO | dsa_chatbot.rag.retriever | Query='Depth-First Search' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 147 |
+
2026-08-02 14:03:52,662 | INFO | dsa_chatbot.agents.router | Routed to COMPARISON: 'Compare merge sort vs quick sort' | topics=['Merge Sort', 'Quick Sort']
|
| 148 |
+
2026-08-02 14:03:52,685 | INFO | dsa_chatbot.rag.retriever | Query='Merge Sort' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 149 |
+
2026-08-02 14:03:52,706 | INFO | dsa_chatbot.rag.retriever | Query='Quick Sort' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 150 |
+
2026-08-02 14:03:52,722 | INFO | dsa_chatbot.agents.router | Routed to COMPARISON: 'Difference between a stack and a queue' | topics=['Queue', 'Stack']
|
| 151 |
+
2026-08-02 14:03:52,749 | INFO | dsa_chatbot.rag.retriever | Query='Queue' | candidates=5 | passed_threshold=5 | threshold=0.45
|
| 152 |
+
2026-08-02 14:03:52,767 | INFO | dsa_chatbot.rag.retriever | Query='Stack' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 153 |
+
2026-08-02 14:03:52,793 | INFO | dsa_chatbot.agents.router | Routed to OUT_OF_SCOPE: "Which one is better, Prim's Algorithm or Kruskal's Algorithm, for MST?"
|
| 154 |
+
2026-08-02 14:03:52,814 | INFO | dsa_chatbot.agents.router | Routed to COMPARISON: 'Compare AVL Tree and Binary Search Tree' | topics=['AVL Tree', 'Binary Search', 'Binary Search Tree']
|
| 155 |
+
2026-08-02 14:03:52,836 | INFO | dsa_chatbot.rag.retriever | Query='AVL Tree' | candidates=5 | passed_threshold=5 | threshold=0.45
|
| 156 |
+
2026-08-02 14:03:52,852 | INFO | dsa_chatbot.rag.retriever | Query='Binary Search' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 157 |
+
2026-08-02 14:03:52,873 | INFO | dsa_chatbot.rag.retriever | Query='Binary Search Tree' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 158 |
+
2026-08-02 14:03:52,886 | INFO | dsa_chatbot.agents.router | Routed to OUT_OF_SCOPE: 'Who won the football World Cup in 2022?'
|
| 159 |
+
2026-08-02 14:03:52,904 | INFO | dsa_chatbot.agents.router | Routed to OUT_OF_SCOPE: "What's a good recipe for chocolate cake?"
|
| 160 |
+
2026-08-02 14:03:52,917 | INFO | dsa_chatbot.agents.router | Routed to OUT_OF_SCOPE: 'What is the capital of France?'
|
| 161 |
+
2026-08-02 14:03:52,924 | INFO | dsa_chatbot.agents.router | Routed to FOLLOWUP: 'Can you explain that again?'
|
| 162 |
+
2026-08-02 14:03:52,949 | INFO | dsa_chatbot.agents.followup | Follow-up query reuses existing conversation context (topics=['Merge Sort']); skipping fresh retrieval.
|
| 163 |
+
2026-08-02 14:03:52,950 | INFO | dsa_chatbot.agents.router | Routed to FOLLOWUP: 'Give me another example'
|
| 164 |
+
2026-08-02 14:03:52,973 | INFO | dsa_chatbot.agents.followup | Follow-up query reuses existing conversation context (topics=['Merge Sort']); skipping fresh retrieval.
|
| 165 |
+
2026-08-02 14:03:52,974 | INFO | dsa_chatbot.__main__ | Evaluation complete: {'total_questions': 20, 'route_accuracy': 0.95, 'avg_topic_recall': 0.95, 'retrieval_latency_ms_avg': 44.4, 'retrieval_latency_ms_p95': 85.11}
|
| 166 |
+
2026-08-02 14:06:23,677 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'Explain how binary search works' | topic=Binary Search
|
| 167 |
+
2026-08-02 14:06:23,808 | INFO | dsa_chatbot.rag.retriever | Query='Explain how binary search works' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 168 |
+
2026-08-02 14:06:23,820 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is the time complexity of merge sort?' | topic=Merge Sort
|
| 169 |
+
2026-08-02 14:06:23,838 | INFO | dsa_chatbot.rag.retriever | Query='What is the time complexity of merge sort?' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 170 |
+
2026-08-02 14:06:23,848 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'Explain quick sort with an example' | topic=Quick Sort
|
| 171 |
+
2026-08-02 14:06:23,872 | INFO | dsa_chatbot.rag.retriever | Query='Explain quick sort with an example' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 172 |
+
2026-08-02 14:06:23,881 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is a Trie used for?' | topic=Trie
|
| 173 |
+
2026-08-02 14:06:23,904 | INFO | dsa_chatbot.rag.retriever | Query='What is a Trie used for?' | candidates=5 | passed_threshold=4 | threshold=0.45
|
| 174 |
+
2026-08-02 14:06:23,917 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is a doubly linked list?' | topic=Doubly Linked List
|
| 175 |
+
2026-08-02 14:06:23,938 | INFO | dsa_chatbot.rag.retriever | Query='What is a doubly linked list?' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 176 |
+
2026-08-02 14:06:23,946 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'Explain how a stack works' | topic=Stack
|
| 177 |
+
2026-08-02 14:06:23,968 | INFO | dsa_chatbot.rag.retriever | Query='Explain how a stack works' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 178 |
+
2026-08-02 14:06:23,980 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is topological sort used for?' | topic=Topological Sort
|
| 179 |
+
2026-08-02 14:06:24,001 | INFO | dsa_chatbot.rag.retriever | Query='What is topological sort used for?' | candidates=5 | passed_threshold=5 | threshold=0.45
|
| 180 |
+
2026-08-02 14:06:24,007 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: "Explain Dijkstra's Algorithm" | topic=Dijkstra's Algorithm
|
| 181 |
+
2026-08-02 14:06:24,029 | INFO | dsa_chatbot.rag.retriever | Query="Explain Dijkstra's Algorithm" | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 182 |
+
2026-08-02 14:06:24,038 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'What is a segment tree?' | topic=Segment Tree
|
| 183 |
+
2026-08-02 14:06:24,065 | INFO | dsa_chatbot.rag.retriever | Query='What is a segment tree?' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 184 |
+
2026-08-02 14:06:24,076 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'Explain the sliding window technique' | topic=Sliding Window Technique
|
| 185 |
+
2026-08-02 14:06:24,099 | INFO | dsa_chatbot.rag.retriever | Query='Explain the sliding window technique' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 186 |
+
2026-08-02 14:06:24,113 | INFO | dsa_chatbot.agents.router | Routed to COMPARISON: 'What is the difference between BFS and DFS?' | topics=['Breadth-First Search', 'Depth-First Search']
|
| 187 |
+
2026-08-02 14:06:24,134 | INFO | dsa_chatbot.rag.retriever | Query='Breadth-First Search' | candidates=5 | passed_threshold=5 | threshold=0.45
|
| 188 |
+
2026-08-02 14:06:24,151 | INFO | dsa_chatbot.rag.retriever | Query='Depth-First Search' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 189 |
+
2026-08-02 14:06:24,166 | INFO | dsa_chatbot.agents.router | Routed to COMPARISON: 'Compare merge sort vs quick sort' | topics=['Merge Sort', 'Quick Sort']
|
| 190 |
+
2026-08-02 14:06:24,190 | INFO | dsa_chatbot.rag.retriever | Query='Merge Sort' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 191 |
+
2026-08-02 14:06:24,215 | INFO | dsa_chatbot.rag.retriever | Query='Quick Sort' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 192 |
+
2026-08-02 14:06:24,229 | INFO | dsa_chatbot.agents.router | Routed to COMPARISON: 'Difference between a stack and a queue' | topics=['Queue', 'Stack']
|
| 193 |
+
2026-08-02 14:06:24,253 | INFO | dsa_chatbot.rag.retriever | Query='Queue' | candidates=5 | passed_threshold=5 | threshold=0.45
|
| 194 |
+
2026-08-02 14:06:24,281 | INFO | dsa_chatbot.rag.retriever | Query='Stack' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 195 |
+
2026-08-02 14:06:24,320 | INFO | dsa_chatbot.agents.router | Routed to COMPARISON: "Which one is better, Prim's Algorithm or Kruskal's Algorithm, for MST?" | topics=["Kruskal's Algorithm (MST)", "Prim's Algorithm (MST)"]
|
| 196 |
+
2026-08-02 14:06:24,354 | INFO | dsa_chatbot.rag.retriever | Query="Kruskal's Algorithm (MST)" | candidates=5 | passed_threshold=5 | threshold=0.45
|
| 197 |
+
2026-08-02 14:06:24,380 | INFO | dsa_chatbot.rag.retriever | Query="Prim's Algorithm (MST)" | candidates=5 | passed_threshold=5 | threshold=0.45
|
| 198 |
+
2026-08-02 14:06:24,395 | INFO | dsa_chatbot.agents.router | Routed to COMPARISON: 'Compare AVL Tree and Binary Search Tree' | topics=['AVL Tree', 'Binary Search', 'Binary Search Tree']
|
| 199 |
+
2026-08-02 14:06:24,419 | INFO | dsa_chatbot.rag.retriever | Query='AVL Tree' | candidates=5 | passed_threshold=5 | threshold=0.45
|
| 200 |
+
2026-08-02 14:06:24,444 | INFO | dsa_chatbot.rag.retriever | Query='Binary Search' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 201 |
+
2026-08-02 14:06:24,465 | INFO | dsa_chatbot.rag.retriever | Query='Binary Search Tree' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 202 |
+
2026-08-02 14:06:24,489 | INFO | dsa_chatbot.agents.router | Routed to OUT_OF_SCOPE: 'Who won the football World Cup in 2022?'
|
| 203 |
+
2026-08-02 14:06:24,503 | INFO | dsa_chatbot.agents.router | Routed to OUT_OF_SCOPE: "What's a good recipe for chocolate cake?"
|
| 204 |
+
2026-08-02 14:06:24,521 | INFO | dsa_chatbot.agents.router | Routed to OUT_OF_SCOPE: 'What is the capital of France?'
|
| 205 |
+
2026-08-02 14:06:24,533 | INFO | dsa_chatbot.agents.router | Routed to FOLLOWUP: 'Can you explain that again?'
|
| 206 |
+
2026-08-02 14:06:24,565 | INFO | dsa_chatbot.agents.followup | Follow-up query reuses existing conversation context (topics=['Merge Sort']); skipping fresh retrieval.
|
| 207 |
+
2026-08-02 14:06:24,566 | INFO | dsa_chatbot.agents.router | Routed to FOLLOWUP: 'Give me another example'
|
| 208 |
+
2026-08-02 14:06:24,590 | INFO | dsa_chatbot.agents.followup | Follow-up query reuses existing conversation context (topics=['Merge Sort']); skipping fresh retrieval.
|
| 209 |
+
2026-08-02 14:06:24,590 | INFO | dsa_chatbot.__main__ | Evaluation complete: {'total_questions': 20, 'route_accuracy': 1.0, 'avg_topic_recall': 1.0, 'retrieval_latency_ms_avg': 46.26, 'retrieval_latency_ms_p95': 101.1}
|
| 210 |
+
2026-08-02 16:00:13,397 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'explain binary search' | topic=Binary Search
|
| 211 |
+
2026-08-02 16:00:14,003 | INFO | dsa_chatbot.rag.retriever | Query='explain binary search' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 212 |
+
2026-08-02 16:00:15,631 | INFO | dsa_chatbot.llm.generate | LLM provider initialized: GroqProvider
|
| 213 |
+
2026-08-02 16:00:17,326 | INFO | dsa_chatbot.chat_service | Chat turn complete | user_id=None | route=single_topic | topics=['Binary Search'] | session_id=None
|
| 214 |
+
2026-08-02 16:00:56,629 | INFO | dsa_chatbot.agents.router | Routed to OUT_OF_SCOPE: '2,3,1,6,5,4 find 5 this using this search step by step'
|
| 215 |
+
2026-08-02 16:00:57,493 | INFO | dsa_chatbot.chat_service | Chat turn complete | user_id=None | route=out_of_scope | topics=[] | session_id=None
|
| 216 |
+
2026-08-02 16:07:37,423 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'explain binary search' | topic=Binary Search
|
| 217 |
+
2026-08-02 16:07:37,480 | INFO | dsa_chatbot.rag.retriever | Query='explain binary search' | candidates=1 | passed_threshold=1 | threshold=0.45
|
| 218 |
+
2026-08-02 16:07:41,129 | INFO | dsa_chatbot.memory.mem0_client | Initializing mem0 (local chroma + huggingface embedder)...
|
| 219 |
+
2026-08-02 16:07:51,945 | ERROR | dsa_chatbot.memory.mem0_client | mem0 get_relevant_context failed for user_id=1
|
| 220 |
+
Traceback (most recent call last):
|
| 221 |
+
File "D:\scratch\chatbot\chatbot\memory\mem0_client.py", line 110, in get_relevant_context
|
| 222 |
+
results = self._get_memory().search(query, user_id=user_id, limit=limit)
|
| 223 |
+
~~~~~~~~~~~~~~~~^^
|
| 224 |
+
File "D:\scratch\chatbot\chatbot\memory\mem0_client.py", line 72, in _get_memory
|
| 225 |
+
self._memory = Memory.from_config(_build_config_dict())
|
| 226 |
+
~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
|
| 227 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\memory\main.py", line 59, in from_config
|
| 228 |
+
return cls(config)
|
| 229 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\memory\main.py", line 37, in __init__
|
| 230 |
+
self.llm = LlmFactory.create(self.config.llm.provider, self.config.llm.config)
|
| 231 |
+
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 232 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\utils\factory.py", line 34, in create
|
| 233 |
+
return llm_instance(base_config)
|
| 234 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\llms\openai.py", line 28, in __init__
|
| 235 |
+
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
| 236 |
+
~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 237 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\openai\_client.py", line 135, in __init__
|
| 238 |
+
raise OpenAIError(
|
| 239 |
+
"The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable"
|
| 240 |
+
)
|
| 241 |
+
openai.OpenAIError: The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable
|
| 242 |
+
2026-08-02 16:07:53,458 | INFO | dsa_chatbot.memory.mem0_client | Initializing mem0 (local chroma + huggingface embedder)...
|
| 243 |
+
2026-08-02 16:07:57,115 | ERROR | dsa_chatbot.memory.mem0_client | mem0 add_interaction failed for user_id=1
|
| 244 |
+
Traceback (most recent call last):
|
| 245 |
+
File "D:\scratch\chatbot\chatbot\memory\mem0_client.py", line 93, in add_interaction
|
| 246 |
+
self._get_memory().add(messages, user_id=user_id, infer=self.infer)
|
| 247 |
+
~~~~~~~~~~~~~~~~^^
|
| 248 |
+
File "D:\scratch\chatbot\chatbot\memory\mem0_client.py", line 72, in _get_memory
|
| 249 |
+
self._memory = Memory.from_config(_build_config_dict())
|
| 250 |
+
~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
|
| 251 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\memory\main.py", line 59, in from_config
|
| 252 |
+
return cls(config)
|
| 253 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\memory\main.py", line 37, in __init__
|
| 254 |
+
self.llm = LlmFactory.create(self.config.llm.provider, self.config.llm.config)
|
| 255 |
+
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 256 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\utils\factory.py", line 34, in create
|
| 257 |
+
return llm_instance(base_config)
|
| 258 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\llms\openai.py", line 28, in __init__
|
| 259 |
+
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
| 260 |
+
~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 261 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\openai\_client.py", line 135, in __init__
|
| 262 |
+
raise OpenAIError(
|
| 263 |
+
"The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable"
|
| 264 |
+
)
|
| 265 |
+
openai.OpenAIError: The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable
|
| 266 |
+
2026-08-02 16:07:57,120 | INFO | dsa_chatbot.chat_service | Chat turn complete | user_id=1 | route=single_topic | topics=['Binary Search'] | session_id=1
|
| 267 |
+
2026-08-02 16:31:21,411 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'explain binary search' | topic=Binary Search
|
| 268 |
+
2026-08-02 16:31:21,670 | INFO | dsa_chatbot.rag.retriever | Query='explain binary search' | candidates=1 | passed_threshold=1 | threshold=-1.00
|
| 269 |
+
2026-08-02 16:31:24,341 | INFO | dsa_chatbot.memory.mem0_client | Initializing mem0 (local chroma + huggingface embedder)...
|
| 270 |
+
2026-08-02 16:31:29,511 | ERROR | dsa_chatbot.memory.mem0_client | mem0 get_relevant_context failed for user_id=1
|
| 271 |
+
Traceback (most recent call last):
|
| 272 |
+
File "D:\scratch\chatbot\chatbot\memory\mem0_client.py", line 110, in get_relevant_context
|
| 273 |
+
results = self._get_memory().search(query, user_id=user_id, limit=limit)
|
| 274 |
+
~~~~~~~~~~~~~~~~^^
|
| 275 |
+
File "D:\scratch\chatbot\chatbot\memory\mem0_client.py", line 72, in _get_memory
|
| 276 |
+
self._memory = Memory.from_config(_build_config_dict())
|
| 277 |
+
~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
|
| 278 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\memory\main.py", line 59, in from_config
|
| 279 |
+
return cls(config)
|
| 280 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\memory\main.py", line 37, in __init__
|
| 281 |
+
self.llm = LlmFactory.create(self.config.llm.provider, self.config.llm.config)
|
| 282 |
+
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 283 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\utils\factory.py", line 34, in create
|
| 284 |
+
return llm_instance(base_config)
|
| 285 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\llms\openai.py", line 28, in __init__
|
| 286 |
+
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
| 287 |
+
~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 288 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\openai\_client.py", line 135, in __init__
|
| 289 |
+
raise OpenAIError(
|
| 290 |
+
"The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable"
|
| 291 |
+
)
|
| 292 |
+
openai.OpenAIError: The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable
|
| 293 |
+
2026-08-02 16:31:30,363 | INFO | dsa_chatbot.llm.generate | LLM provider initialized: GroqProvider
|
| 294 |
+
2026-08-02 16:31:31,945 | INFO | dsa_chatbot.memory.mem0_client | Initializing mem0 (local chroma + huggingface embedder)...
|
| 295 |
+
2026-08-02 16:31:35,540 | ERROR | dsa_chatbot.memory.mem0_client | mem0 add_interaction failed for user_id=1
|
| 296 |
+
Traceback (most recent call last):
|
| 297 |
+
File "D:\scratch\chatbot\chatbot\memory\mem0_client.py", line 93, in add_interaction
|
| 298 |
+
self._get_memory().add(messages, user_id=user_id, infer=self.infer)
|
| 299 |
+
~~~~~~~~~~~~~~~~^^
|
| 300 |
+
File "D:\scratch\chatbot\chatbot\memory\mem0_client.py", line 72, in _get_memory
|
| 301 |
+
self._memory = Memory.from_config(_build_config_dict())
|
| 302 |
+
~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
|
| 303 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\memory\main.py", line 59, in from_config
|
| 304 |
+
return cls(config)
|
| 305 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\memory\main.py", line 37, in __init__
|
| 306 |
+
self.llm = LlmFactory.create(self.config.llm.provider, self.config.llm.config)
|
| 307 |
+
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 308 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\utils\factory.py", line 34, in create
|
| 309 |
+
return llm_instance(base_config)
|
| 310 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\llms\openai.py", line 28, in __init__
|
| 311 |
+
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
| 312 |
+
~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 313 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\openai\_client.py", line 135, in __init__
|
| 314 |
+
raise OpenAIError(
|
| 315 |
+
"The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable"
|
| 316 |
+
)
|
| 317 |
+
openai.OpenAIError: The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable
|
| 318 |
+
2026-08-02 16:31:35,545 | INFO | dsa_chatbot.chat_service | Chat turn complete | user_id=1 | route=single_topic | topics=['Binary Search'] | session_id=2
|
| 319 |
+
2026-08-02 16:32:22,442 | INFO | dsa_chatbot.agents.router | Routed to FOLLOWUP: 'in this array [65,32,43,23,78,96] how will you find 23 using this search'
|
| 320 |
+
2026-08-02 16:32:24,207 | INFO | dsa_chatbot.agents.followup | Follow-up query reuses existing conversation context (topics=['Binary Search', 'Linear Search']); skipping fresh retrieval.
|
| 321 |
+
2026-08-02 16:32:24,208 | INFO | dsa_chatbot.memory.mem0_client | Initializing mem0 (local chroma + huggingface embedder)...
|
| 322 |
+
2026-08-02 16:32:27,835 | ERROR | dsa_chatbot.memory.mem0_client | mem0 get_relevant_context failed for user_id=1
|
| 323 |
+
Traceback (most recent call last):
|
| 324 |
+
File "D:\scratch\chatbot\chatbot\memory\mem0_client.py", line 110, in get_relevant_context
|
| 325 |
+
results = self._get_memory().search(query, user_id=user_id, limit=limit)
|
| 326 |
+
~~~~~~~~~~~~~~~~^^
|
| 327 |
+
File "D:\scratch\chatbot\chatbot\memory\mem0_client.py", line 72, in _get_memory
|
| 328 |
+
self._memory = Memory.from_config(_build_config_dict())
|
| 329 |
+
~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
|
| 330 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\memory\main.py", line 59, in from_config
|
| 331 |
+
return cls(config)
|
| 332 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\memory\main.py", line 37, in __init__
|
| 333 |
+
self.llm = LlmFactory.create(self.config.llm.provider, self.config.llm.config)
|
| 334 |
+
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 335 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\utils\factory.py", line 34, in create
|
| 336 |
+
return llm_instance(base_config)
|
| 337 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\llms\openai.py", line 28, in __init__
|
| 338 |
+
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
| 339 |
+
~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 340 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\openai\_client.py", line 135, in __init__
|
| 341 |
+
raise OpenAIError(
|
| 342 |
+
"The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable"
|
| 343 |
+
)
|
| 344 |
+
openai.OpenAIError: The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable
|
| 345 |
+
2026-08-02 16:32:29,805 | INFO | dsa_chatbot.memory.mem0_client | Initializing mem0 (local chroma + huggingface embedder)...
|
| 346 |
+
2026-08-02 16:32:33,863 | ERROR | dsa_chatbot.memory.mem0_client | mem0 add_interaction failed for user_id=1
|
| 347 |
+
Traceback (most recent call last):
|
| 348 |
+
File "D:\scratch\chatbot\chatbot\memory\mem0_client.py", line 93, in add_interaction
|
| 349 |
+
self._get_memory().add(messages, user_id=user_id, infer=self.infer)
|
| 350 |
+
~~~~~~~~~~~~~~~~^^
|
| 351 |
+
File "D:\scratch\chatbot\chatbot\memory\mem0_client.py", line 72, in _get_memory
|
| 352 |
+
self._memory = Memory.from_config(_build_config_dict())
|
| 353 |
+
~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
|
| 354 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\memory\main.py", line 59, in from_config
|
| 355 |
+
return cls(config)
|
| 356 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\memory\main.py", line 37, in __init__
|
| 357 |
+
self.llm = LlmFactory.create(self.config.llm.provider, self.config.llm.config)
|
| 358 |
+
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 359 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\utils\factory.py", line 34, in create
|
| 360 |
+
return llm_instance(base_config)
|
| 361 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\llms\openai.py", line 28, in __init__
|
| 362 |
+
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
| 363 |
+
~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 364 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\openai\_client.py", line 135, in __init__
|
| 365 |
+
raise OpenAIError(
|
| 366 |
+
"The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable"
|
| 367 |
+
)
|
| 368 |
+
openai.OpenAIError: The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable
|
| 369 |
+
2026-08-02 16:32:33,869 | INFO | dsa_chatbot.chat_service | Chat turn complete | user_id=1 | route=followup | topics=[] | session_id=2
|
| 370 |
+
2026-08-02 16:33:48,648 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'compare this search with linear search' | topic=Linear Search
|
| 371 |
+
2026-08-02 16:33:48,715 | INFO | dsa_chatbot.rag.retriever | Query='compare this search with linear search' | candidates=1 | passed_threshold=1 | threshold=-1.00
|
| 372 |
+
2026-08-02 16:33:48,716 | INFO | dsa_chatbot.memory.mem0_client | Initializing mem0 (local chroma + huggingface embedder)...
|
| 373 |
+
2026-08-02 16:33:52,390 | ERROR | dsa_chatbot.memory.mem0_client | mem0 get_relevant_context failed for user_id=1
|
| 374 |
+
Traceback (most recent call last):
|
| 375 |
+
File "D:\scratch\chatbot\chatbot\memory\mem0_client.py", line 110, in get_relevant_context
|
| 376 |
+
results = self._get_memory().search(query, user_id=user_id, limit=limit)
|
| 377 |
+
~~~~~~~~~~~~~~~~^^
|
| 378 |
+
File "D:\scratch\chatbot\chatbot\memory\mem0_client.py", line 72, in _get_memory
|
| 379 |
+
self._memory = Memory.from_config(_build_config_dict())
|
| 380 |
+
~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
|
| 381 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\memory\main.py", line 59, in from_config
|
| 382 |
+
return cls(config)
|
| 383 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\memory\main.py", line 37, in __init__
|
| 384 |
+
self.llm = LlmFactory.create(self.config.llm.provider, self.config.llm.config)
|
| 385 |
+
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 386 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\utils\factory.py", line 34, in create
|
| 387 |
+
return llm_instance(base_config)
|
| 388 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\llms\openai.py", line 28, in __init__
|
| 389 |
+
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
| 390 |
+
~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 391 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\openai\_client.py", line 135, in __init__
|
| 392 |
+
raise OpenAIError(
|
| 393 |
+
"The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable"
|
| 394 |
+
)
|
| 395 |
+
openai.OpenAIError: The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable
|
| 396 |
+
2026-08-02 16:33:54,095 | INFO | dsa_chatbot.memory.mem0_client | Initializing mem0 (local chroma + huggingface embedder)...
|
| 397 |
+
2026-08-02 16:33:58,206 | ERROR | dsa_chatbot.memory.mem0_client | mem0 add_interaction failed for user_id=1
|
| 398 |
+
Traceback (most recent call last):
|
| 399 |
+
File "D:\scratch\chatbot\chatbot\memory\mem0_client.py", line 93, in add_interaction
|
| 400 |
+
self._get_memory().add(messages, user_id=user_id, infer=self.infer)
|
| 401 |
+
~~~~~~~~~~~~~~~~^^
|
| 402 |
+
File "D:\scratch\chatbot\chatbot\memory\mem0_client.py", line 72, in _get_memory
|
| 403 |
+
self._memory = Memory.from_config(_build_config_dict())
|
| 404 |
+
~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
|
| 405 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\memory\main.py", line 59, in from_config
|
| 406 |
+
return cls(config)
|
| 407 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\memory\main.py", line 37, in __init__
|
| 408 |
+
self.llm = LlmFactory.create(self.config.llm.provider, self.config.llm.config)
|
| 409 |
+
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 410 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\utils\factory.py", line 34, in create
|
| 411 |
+
return llm_instance(base_config)
|
| 412 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\mem0\llms\openai.py", line 28, in __init__
|
| 413 |
+
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
| 414 |
+
~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 415 |
+
File "D:\scratch\chatbot\chatbot\venv\Lib\site-packages\openai\_client.py", line 135, in __init__
|
| 416 |
+
raise OpenAIError(
|
| 417 |
+
"The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable"
|
| 418 |
+
)
|
| 419 |
+
openai.OpenAIError: The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable
|
| 420 |
+
2026-08-02 16:33:58,212 | INFO | dsa_chatbot.chat_service | Chat turn complete | user_id=1 | route=single_topic | topics=['Linear Search'] | session_id=2
|
| 421 |
+
2026-08-02 19:53:48,973 | INFO | dsa_chatbot.agents.router | Routed to OUT_OF_SCOPE: 'how are you'
|
| 422 |
+
2026-08-02 19:53:49,894 | INFO | dsa_chatbot.llm.generate | LLM provider initialized: GroqProvider
|
| 423 |
+
2026-08-02 19:53:51,450 | INFO | dsa_chatbot.chat_service | Chat turn complete | user_id=None | route=out_of_scope | topics=[] | session_id=None
|
| 424 |
+
2026-08-02 22:34:22,716 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: "explain dijikstra's algorithm" | topic=Dijkstra's Algorithm
|
| 425 |
+
2026-08-02 22:34:23,350 | INFO | dsa_chatbot.rag.retriever | Query="explain dijikstra's algorithm" | candidates=1 | passed_threshold=1 | threshold=-1.00
|
| 426 |
+
2026-08-02 22:34:24,785 | INFO | dsa_chatbot.llm.generate | LLM provider initialized: GroqProvider
|
| 427 |
+
2026-08-02 22:34:26,608 | INFO | dsa_chatbot.chat_service | Chat turn complete | user_id=None | route=single_topic | topics=["Dijkstra's Algorithm"] | session_id=None
|
| 428 |
+
2026-08-02 22:41:13,334 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'explain bfs' | topic=Breadth-First Search
|
| 429 |
+
2026-08-02 22:41:13,572 | INFO | dsa_chatbot.rag.retriever | Query='explain bfs' | candidates=5 | passed_threshold=5 | threshold=-1.00
|
| 430 |
+
2026-08-02 22:41:14,419 | INFO | dsa_chatbot.llm.generate | LLM provider initialized: GroqProvider
|
| 431 |
+
2026-08-02 22:41:15,798 | INFO | dsa_chatbot.chat_service | Chat turn complete | user_id=None | route=single_topic | topics=['Breadth-First Search'] | session_id=None
|
| 432 |
+
2026-08-02 22:41:56,906 | INFO | dsa_chatbot.agents.router | Routed to OUT_OF_SCOPE: 'compare with other graph parsing algorithms'
|
| 433 |
+
2026-08-02 22:41:57,470 | INFO | dsa_chatbot.chat_service | Chat turn complete | user_id=None | route=out_of_scope | topics=[] | session_id=None
|
| 434 |
+
2026-08-02 22:51:55,471 | INFO | dsa_chatbot.agents.router | Routed to SINGLE_TOPIC: 'explain dfs' | topic=Depth-First Search
|
| 435 |
+
2026-08-02 22:51:55,687 | INFO | dsa_chatbot.rag.retriever | Query='explain dfs' | candidates=1 | passed_threshold=1 | threshold=-1.00
|
| 436 |
+
2026-08-02 22:51:56,559 | INFO | dsa_chatbot.llm.generate | LLM provider initialized: GroqProvider
|
| 437 |
+
2026-08-02 22:51:57,814 | INFO | dsa_chatbot.chat_service | Chat turn complete | user_id=None | route=single_topic | topics=['Depth-First Search'] | session_id=None
|
| 438 |
+
2026-08-02 22:52:27,161 | INFO | dsa_chatbot.agents.router | Routed to FOLLOWUP: 'compare it with other graph traversal algorithms'
|
| 439 |
+
2026-08-02 22:52:28,250 | INFO | dsa_chatbot.agents.followup | Follow-up query reuses existing conversation context (topics=['Depth-First Search', 'Stack']); skipping fresh retrieval.
|
| 440 |
+
2026-08-02 22:52:29,862 | INFO | dsa_chatbot.chat_service | Chat turn complete | user_id=None | route=followup | topics=[] | session_id=None
|
logs/logger.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Centralized logging setup.
|
| 3 |
+
|
| 4 |
+
Every module does `from logs.logger import get_logger` and
|
| 5 |
+
`logger = get_logger(__name__)` rather than configuring logging itself.
|
| 6 |
+
Writes to both console and logs/app.log (paths from config.py).
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import logging
|
| 10 |
+
import os
|
| 11 |
+
import sys
|
| 12 |
+
|
| 13 |
+
import config
|
| 14 |
+
|
| 15 |
+
_CONFIGURED = False
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _configure_root():
|
| 19 |
+
global _CONFIGURED
|
| 20 |
+
if _CONFIGURED:
|
| 21 |
+
return
|
| 22 |
+
|
| 23 |
+
os.makedirs(config.LOG_DIR, exist_ok=True)
|
| 24 |
+
|
| 25 |
+
root = logging.getLogger("dsa_chatbot")
|
| 26 |
+
root.setLevel(config.LOG_LEVEL)
|
| 27 |
+
root.propagate = False
|
| 28 |
+
|
| 29 |
+
formatter = logging.Formatter(
|
| 30 |
+
"%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
if not root.handlers:
|
| 34 |
+
stream_handler = logging.StreamHandler(sys.stdout)
|
| 35 |
+
stream_handler.setFormatter(formatter)
|
| 36 |
+
root.addHandler(stream_handler)
|
| 37 |
+
|
| 38 |
+
file_handler = logging.FileHandler(config.LOG_FILE_PATH, encoding="utf-8")
|
| 39 |
+
file_handler.setFormatter(formatter)
|
| 40 |
+
root.addHandler(file_handler)
|
| 41 |
+
|
| 42 |
+
_CONFIGURED = True
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def get_logger(name: str) -> logging.Logger:
|
| 46 |
+
"""Returns a named logger nested under the shared 'dsa_chatbot' root."""
|
| 47 |
+
_configure_root()
|
| 48 |
+
return logging.getLogger(f"dsa_chatbot.{name}")
|
memory/__init__.py
ADDED
|
File without changes
|
memory/mem0_client.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
mem0 client wrapper.
|
| 3 |
+
|
| 4 |
+
Responsibility boundary (do not blur this):
|
| 5 |
+
- mem0 -> user preferences + long-term conversational memory
|
| 6 |
+
(e.g. "prefers beginner explanations", "likes step-by-step examples")
|
| 7 |
+
- ChromaDB -> DSA knowledge embeddings (rag/retriever.py) — NEVER touched here
|
| 8 |
+
- SQLite -> raw message/session transcripts (history/) — NEVER touched here
|
| 9 |
+
|
| 10 |
+
mem0 runs in local/open-source mode: a local Chroma vector store (separate
|
| 11 |
+
collection from the knowledge base) + a local sentence-transformers
|
| 12 |
+
embedder (same model as the KB for consistency, configurable independently
|
| 13 |
+
if needed).
|
| 14 |
+
|
| 15 |
+
Fact extraction (infer=True) would use an LLM to summarize what to
|
| 16 |
+
remember. To avoid depending on the (expensive, 4-bit-quantized) local
|
| 17 |
+
Mistral model just to log a preference, this wrapper uses infer=False by
|
| 18 |
+
default — messages are stored as-is and semantic search over them still
|
| 19 |
+
works for retrieval. Set infer=True if you want mem0 to run LLM-based
|
| 20 |
+
fact extraction using the same provider configured in llm/generate.py.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
import os
|
| 24 |
+
|
| 25 |
+
import config
|
| 26 |
+
from logs.logger import get_logger
|
| 27 |
+
|
| 28 |
+
logger = get_logger(__name__)
|
| 29 |
+
|
| 30 |
+
_MEM0_COLLECTION_NAME = "user_preferences_memory"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _build_config_dict() -> dict:
|
| 34 |
+
os.makedirs(config.MEM0_LOCAL_STORAGE_DIR, exist_ok=True)
|
| 35 |
+
return {
|
| 36 |
+
"vector_store": {
|
| 37 |
+
"provider": "chroma",
|
| 38 |
+
"config": {
|
| 39 |
+
"collection_name": _MEM0_COLLECTION_NAME,
|
| 40 |
+
"path": os.path.join(config.MEM0_LOCAL_STORAGE_DIR, "chroma_db"),
|
| 41 |
+
},
|
| 42 |
+
},
|
| 43 |
+
"embedder": {
|
| 44 |
+
"provider": "huggingface",
|
| 45 |
+
"config": {
|
| 46 |
+
"model": f"sentence-transformers/{config.EMBEDDING_MODEL_NAME}",
|
| 47 |
+
},
|
| 48 |
+
},
|
| 49 |
+
"history_db_path": os.path.join(config.MEM0_LOCAL_STORAGE_DIR, "mem0_history.db"),
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class Mem0Client:
|
| 54 |
+
"""
|
| 55 |
+
Thin wrapper around mem0.Memory scoped to a single responsibility:
|
| 56 |
+
user preferences and long-term conversational memory.
|
| 57 |
+
"""
|
| 58 |
+
|
| 59 |
+
def __init__(self, infer: bool = False):
|
| 60 |
+
self.infer = infer
|
| 61 |
+
self._memory = None
|
| 62 |
+
self._enabled = config.USE_MEM0
|
| 63 |
+
|
| 64 |
+
if not self._enabled:
|
| 65 |
+
logger.info("USE_MEM0 is False — Mem0Client will be a no-op.")
|
| 66 |
+
|
| 67 |
+
def _get_memory(self):
|
| 68 |
+
if self._memory is None:
|
| 69 |
+
from mem0 import Memory
|
| 70 |
+
|
| 71 |
+
logger.info("Initializing mem0 (local chroma + huggingface embedder)...")
|
| 72 |
+
self._memory = Memory.from_config(_build_config_dict())
|
| 73 |
+
return self._memory
|
| 74 |
+
|
| 75 |
+
def add_interaction(self, user_id: str, user_message: str, assistant_message: str = None) -> None:
|
| 76 |
+
"""
|
| 77 |
+
Store a turn of conversation for long-term memory purposes (NOT
|
| 78 |
+
transcript storage — that's history/service.py + SQLite).
|
| 79 |
+
|
| 80 |
+
Only call this for things worth remembering long-term (preferences,
|
| 81 |
+
recurring topics of interest, stated skill level) — not every raw
|
| 82 |
+
message. The router/chat layer decides when this is worth calling;
|
| 83 |
+
this wrapper doesn't filter content itself.
|
| 84 |
+
"""
|
| 85 |
+
if not self._enabled:
|
| 86 |
+
return
|
| 87 |
+
|
| 88 |
+
messages = [{"role": "user", "content": user_message}]
|
| 89 |
+
if assistant_message:
|
| 90 |
+
messages.append({"role": "assistant", "content": assistant_message})
|
| 91 |
+
|
| 92 |
+
try:
|
| 93 |
+
self._get_memory().add(messages, user_id=user_id, infer=self.infer)
|
| 94 |
+
except Exception:
|
| 95 |
+
logger.exception("mem0 add_interaction failed for user_id=%s", user_id)
|
| 96 |
+
|
| 97 |
+
def get_relevant_context(self, user_id: str, query: str, limit: int = 5) -> list:
|
| 98 |
+
"""
|
| 99 |
+
Returns a list of plain-text memory strings relevant to `query`,
|
| 100 |
+
ready to hand to llm/prompts.py's mem0_context parameter.
|
| 101 |
+
|
| 102 |
+
Returns [] if mem0 is disabled, the user has no memories yet, or a
|
| 103 |
+
lookup error occurs — callers should treat that as "no memory
|
| 104 |
+
context available", not as an error condition.
|
| 105 |
+
"""
|
| 106 |
+
if not self._enabled:
|
| 107 |
+
return []
|
| 108 |
+
|
| 109 |
+
try:
|
| 110 |
+
results = self._get_memory().search(query, user_id=user_id, limit=limit)
|
| 111 |
+
except Exception:
|
| 112 |
+
logger.exception("mem0 get_relevant_context failed for user_id=%s", user_id)
|
| 113 |
+
return []
|
| 114 |
+
|
| 115 |
+
# mem0's search() returns {"results": [{"memory": "...", "score": ...}, ...]}
|
| 116 |
+
# in v2, or a plain list in some versions — handle both.
|
| 117 |
+
items = results.get("results", results) if isinstance(results, dict) else results
|
| 118 |
+
return [item.get("memory", "") for item in items if item.get("memory")]
|
| 119 |
+
|
| 120 |
+
def delete_all_for_user(self, user_id: str) -> None:
|
| 121 |
+
"""Useful for account deletion / a 'forget me' feature."""
|
| 122 |
+
if not self._enabled:
|
| 123 |
+
return
|
| 124 |
+
try:
|
| 125 |
+
self._get_memory().delete_all(user_id=user_id)
|
| 126 |
+
except Exception:
|
| 127 |
+
logger.exception("mem0 delete_all_for_user failed for user_id=%s", user_id)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
_CLIENT_SINGLETON = None
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def get_mem0_client() -> Mem0Client:
|
| 134 |
+
global _CLIENT_SINGLETON
|
| 135 |
+
if _CLIENT_SINGLETON is None:
|
| 136 |
+
_CLIENT_SINGLETON = Mem0Client()
|
| 137 |
+
return _CLIENT_SINGLETON
|
rag/__init__.py
ADDED
|
File without changes
|
rag/build_index.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
RAG index builder.
|
| 3 |
+
|
| 4 |
+
Reads every knowledge-base .txt file, splits it into semantic chunks based
|
| 5 |
+
on the existing structural headings (DEFINITION, TIME_COMPLEXITY,
|
| 6 |
+
SPACE_COMPLEXITY, USE_WHEN, EXAMPLE) rather than arbitrary fixed-size
|
| 7 |
+
windows, attaches per-topic metadata from topic_metadata.json, embeds each
|
| 8 |
+
chunk with sentence-transformers, and stores everything in a persistent
|
| 9 |
+
ChromaDB collection.
|
| 10 |
+
|
| 11 |
+
This script NEVER modifies the source .txt files. They are treated as
|
| 12 |
+
immutable. Run it whenever the knowledge base or topic_metadata.json
|
| 13 |
+
changes:
|
| 14 |
+
|
| 15 |
+
python -m rag.build_index
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import json
|
| 19 |
+
import os
|
| 20 |
+
import re
|
| 21 |
+
import sys
|
| 22 |
+
|
| 23 |
+
import chromadb
|
| 24 |
+
from sentence_transformers import SentenceTransformer
|
| 25 |
+
|
| 26 |
+
# Allow running as a standalone script (python rag/build_index.py) as well
|
| 27 |
+
# as a module (python -m rag.build_index).
|
| 28 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 29 |
+
|
| 30 |
+
import config
|
| 31 |
+
from logs.logger import get_logger
|
| 32 |
+
|
| 33 |
+
logger = get_logger(__name__)
|
| 34 |
+
|
| 35 |
+
# Matches a section header line, e.g. "DEFINITION:" or "TIME_COMPLEXITY:"
|
| 36 |
+
_HEADER_PATTERN = re.compile(
|
| 37 |
+
r"^(" + "|".join(config.KB_SECTION_HEADERS) + r"):\s*$", re.MULTILINE
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
_TOPIC_LINE_PATTERN = re.compile(r"^TOPIC:\s*(.+)$", re.MULTILINE)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _parse_sections(file_text: str) -> dict:
|
| 44 |
+
"""
|
| 45 |
+
Split a KB file's text into {section_name: section_body} using the
|
| 46 |
+
structural headers defined in config.KB_SECTION_HEADERS.
|
| 47 |
+
|
| 48 |
+
Any text before the first recognized header (e.g. a "TOPIC:" line) is
|
| 49 |
+
ignored here — the topic name is expected to come from
|
| 50 |
+
topic_metadata.json, not parsed out of the file body.
|
| 51 |
+
"""
|
| 52 |
+
matches = list(_HEADER_PATTERN.finditer(file_text))
|
| 53 |
+
sections = {}
|
| 54 |
+
|
| 55 |
+
for i, match in enumerate(matches):
|
| 56 |
+
header = match.group(1)
|
| 57 |
+
start = match.end()
|
| 58 |
+
end = matches[i + 1].start() if i + 1 < len(matches) else len(file_text)
|
| 59 |
+
body = file_text[start:end].strip()
|
| 60 |
+
if body:
|
| 61 |
+
sections[header] = body
|
| 62 |
+
|
| 63 |
+
return sections
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _load_topic_metadata() -> dict:
|
| 67 |
+
if not os.path.exists(config.TOPIC_METADATA_PATH):
|
| 68 |
+
raise FileNotFoundError(
|
| 69 |
+
f"topic_metadata.json not found at {config.TOPIC_METADATA_PATH}. "
|
| 70 |
+
"Every knowledge-base file must have a matching entry."
|
| 71 |
+
)
|
| 72 |
+
with open(config.TOPIC_METADATA_PATH, "r", encoding="utf-8") as f:
|
| 73 |
+
return json.load(f)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _list_kb_files() -> list:
|
| 77 |
+
if not os.path.isdir(config.KNOWLEDGE_BASE_DIR):
|
| 78 |
+
raise FileNotFoundError(
|
| 79 |
+
f"Knowledge base directory not found: {config.KNOWLEDGE_BASE_DIR}"
|
| 80 |
+
)
|
| 81 |
+
return sorted(
|
| 82 |
+
f
|
| 83 |
+
for f in os.listdir(config.KNOWLEDGE_BASE_DIR)
|
| 84 |
+
if f.endswith(".txt")
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def build_chunks() -> list:
|
| 89 |
+
"""
|
| 90 |
+
Returns a list of dicts, each representing one chunk ready for embedding:
|
| 91 |
+
{
|
| 92 |
+
"id": "binary_search.txt::DEFINITION",
|
| 93 |
+
"text": "...",
|
| 94 |
+
"metadata": {
|
| 95 |
+
"topic": "...",
|
| 96 |
+
"category": "...",
|
| 97 |
+
"section": "...",
|
| 98 |
+
"difficulty": "...",
|
| 99 |
+
"source": "binary_search.txt",
|
| 100 |
+
"chunk_id": 0
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
"""
|
| 104 |
+
topic_metadata = _load_topic_metadata()
|
| 105 |
+
kb_files = _list_kb_files()
|
| 106 |
+
|
| 107 |
+
missing_metadata = [f for f in kb_files if f not in topic_metadata]
|
| 108 |
+
if missing_metadata:
|
| 109 |
+
logger.warning(
|
| 110 |
+
"The following KB files have no topic_metadata.json entry and "
|
| 111 |
+
"will be skipped: %s",
|
| 112 |
+
missing_metadata,
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
chunks = []
|
| 116 |
+
chunk_counter = 0
|
| 117 |
+
|
| 118 |
+
for filename in kb_files:
|
| 119 |
+
if filename not in topic_metadata:
|
| 120 |
+
continue
|
| 121 |
+
|
| 122 |
+
file_path = os.path.join(config.KNOWLEDGE_BASE_DIR, filename)
|
| 123 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
| 124 |
+
file_text = f.read()
|
| 125 |
+
|
| 126 |
+
sections = _parse_sections(file_text)
|
| 127 |
+
if not sections:
|
| 128 |
+
logger.warning(
|
| 129 |
+
"No recognized section headers (%s) found in %s. Skipping.",
|
| 130 |
+
config.KB_SECTION_HEADERS,
|
| 131 |
+
filename,
|
| 132 |
+
)
|
| 133 |
+
continue
|
| 134 |
+
|
| 135 |
+
meta = topic_metadata[filename]
|
| 136 |
+
|
| 137 |
+
for section_name, section_body in sections.items():
|
| 138 |
+
chunk = {
|
| 139 |
+
"id": f"{filename}::{section_name}",
|
| 140 |
+
"text": f"{meta.get('topic', filename)} - {section_name}: {section_body}",
|
| 141 |
+
"metadata": {
|
| 142 |
+
"topic": meta.get("topic", ""),
|
| 143 |
+
"category": meta.get("category", ""),
|
| 144 |
+
"section": section_name,
|
| 145 |
+
"difficulty": meta.get("difficulty", ""),
|
| 146 |
+
"source": filename,
|
| 147 |
+
"prerequisites": ", ".join(meta.get("prerequisites", [])),
|
| 148 |
+
"chunk_id": chunk_counter,
|
| 149 |
+
},
|
| 150 |
+
}
|
| 151 |
+
chunks.append(chunk)
|
| 152 |
+
chunk_counter += 1
|
| 153 |
+
|
| 154 |
+
logger.info("Built %d chunks from %d knowledge-base files.", len(chunks), len(kb_files))
|
| 155 |
+
return chunks
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def build_index(reset: bool = True) -> None:
|
| 159 |
+
"""
|
| 160 |
+
Embed all chunks and persist them into the ChromaDB collection.
|
| 161 |
+
|
| 162 |
+
Args:
|
| 163 |
+
reset: if True, drops and recreates the collection so the index is
|
| 164 |
+
rebuilt from scratch (safe default whenever the KB or
|
| 165 |
+
metadata changes). If False, upserts into the existing
|
| 166 |
+
collection.
|
| 167 |
+
"""
|
| 168 |
+
os.makedirs(config.CHROMA_DB_DIR, exist_ok=True)
|
| 169 |
+
|
| 170 |
+
logger.info("Loading embedding model: %s", config.EMBEDDING_MODEL_NAME)
|
| 171 |
+
embedder = SentenceTransformer(config.EMBEDDING_MODEL_NAME)
|
| 172 |
+
|
| 173 |
+
client = chromadb.PersistentClient(path=config.CHROMA_DB_DIR)
|
| 174 |
+
|
| 175 |
+
if reset:
|
| 176 |
+
try:
|
| 177 |
+
client.delete_collection(config.CHROMA_COLLECTION_NAME)
|
| 178 |
+
logger.info("Existing collection '%s' deleted.", config.CHROMA_COLLECTION_NAME)
|
| 179 |
+
except Exception:
|
| 180 |
+
pass # collection didn't exist yet
|
| 181 |
+
|
| 182 |
+
collection = client.get_or_create_collection(name=config.CHROMA_COLLECTION_NAME)
|
| 183 |
+
|
| 184 |
+
chunks = build_chunks()
|
| 185 |
+
if not chunks:
|
| 186 |
+
logger.warning("No chunks produced — index will be empty.")
|
| 187 |
+
return
|
| 188 |
+
|
| 189 |
+
texts = [c["text"] for c in chunks]
|
| 190 |
+
ids = [c["id"] for c in chunks]
|
| 191 |
+
metadatas = [c["metadata"] for c in chunks]
|
| 192 |
+
|
| 193 |
+
logger.info("Embedding %d chunks...", len(texts))
|
| 194 |
+
embeddings = embedder.encode(texts, show_progress_bar=False).tolist()
|
| 195 |
+
|
| 196 |
+
collection.upsert(
|
| 197 |
+
ids=ids,
|
| 198 |
+
embeddings=embeddings,
|
| 199 |
+
documents=texts,
|
| 200 |
+
metadatas=metadatas,
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
logger.info(
|
| 204 |
+
"Index build complete. Collection '%s' now has %d chunks.",
|
| 205 |
+
config.CHROMA_COLLECTION_NAME,
|
| 206 |
+
collection.count(),
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
if __name__ == "__main__":
|
| 211 |
+
build_index(reset=True)
|
rag/generate.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Batch-generates DSA knowledge base .txt files using the Groq API.
|
| 3 |
+
|
| 4 |
+
Setup:
|
| 5 |
+
pip install groq python-dotenv
|
| 6 |
+
Create a .env file with: GROQ_API_KEY=your_key_here
|
| 7 |
+
|
| 8 |
+
Run:
|
| 9 |
+
python generate_knowledge_base.py
|
| 10 |
+
|
| 11 |
+
Output:
|
| 12 |
+
Files are written to ./knowledge_base/<topic_snake_case>.txt
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
import re
|
| 17 |
+
import time
|
| 18 |
+
from dotenv import load_dotenv
|
| 19 |
+
from groq import Groq
|
| 20 |
+
|
| 21 |
+
load_dotenv()
|
| 22 |
+
|
| 23 |
+
client = Groq(api_key=os.environ.get("GROQ_API_KEY"))
|
| 24 |
+
MODEL = "llama-3.3-70b-versatile" # swap for another Groq-hosted model if you prefer
|
| 25 |
+
OUTPUT_DIR = "knowledge_base"
|
| 26 |
+
DELAY_BETWEEN_CALLS = 1.5 # seconds, be nice to rate limits
|
| 27 |
+
|
| 28 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 29 |
+
|
| 30 |
+
# ── PROMPT TEMPLATE ─────────────────────────────────────────────────────────
|
| 31 |
+
PROMPT_TEMPLATE = """You are writing a knowledge base entry for a DSA (Data Structures & Algorithms)
|
| 32 |
+
tutoring RAG chatbot. Write in your own original words — do NOT copy verbatim from
|
| 33 |
+
GeeksforGeeks, textbooks, or any other source. Paraphrase and synthesize instead.
|
| 34 |
+
Keep explanations beginner-friendly but technically accurate.
|
| 35 |
+
|
| 36 |
+
Generate the entry for this topic: {topic}
|
| 37 |
+
|
| 38 |
+
Output ONLY in the exact following plain text structure, no markdown formatting,
|
| 39 |
+
no extra commentary before or after:
|
| 40 |
+
|
| 41 |
+
TOPIC: {topic}
|
| 42 |
+
|
| 43 |
+
DEFINITION:
|
| 44 |
+
<2-3 sentence clear definition of what it is and what problem it solves>
|
| 45 |
+
|
| 46 |
+
TIME_COMPLEXITY:
|
| 47 |
+
<Best/Average/Worst case if they differ, otherwise a single Big-O with brief justification>
|
| 48 |
+
|
| 49 |
+
SPACE_COMPLEXITY:
|
| 50 |
+
<Big-O with brief note on what's using the space>
|
| 51 |
+
|
| 52 |
+
USE_WHEN:
|
| 53 |
+
<1-2 sentences: scenarios/conditions where this is the right tool>
|
| 54 |
+
|
| 55 |
+
AVOID_WHEN:
|
| 56 |
+
<1-2 sentences: scenarios where this is a poor choice, and what to use instead>
|
| 57 |
+
|
| 58 |
+
EXAMPLE:
|
| 59 |
+
<A small, concrete worked example (3-6 elements/nodes max), shown as a step-by-step
|
| 60 |
+
walkthrough with intermediate states, ending in a clear result marked with a checkmark.
|
| 61 |
+
Use simple ASCII/text formatting (arrows, brackets, indentation) — no images.>
|
| 62 |
+
|
| 63 |
+
REAL_WORLD_ANALOGY:
|
| 64 |
+
<1-2 sentence relatable comparison to something non-technical>
|
| 65 |
+
|
| 66 |
+
SOURCE_NOTE:
|
| 67 |
+
<Optional: e.g. "Concepts referenced from CLRS Introduction to Algorithms" or
|
| 68 |
+
"GeeksforGeeks - {topic} (paraphrased, no verbatim text used)". Leave blank if not applicable.>
|
| 69 |
+
"""
|
| 70 |
+
|
| 71 |
+
# ── TOPIC LIST ───────────────────────────────────────────────────────────────
|
| 72 |
+
TOPICS = [
|
| 73 |
+
# Arrays & Searching
|
| 74 |
+
"Linear Search", "Binary Search", "Two Pointer Technique", "Sliding Window Technique",
|
| 75 |
+
# Sorting
|
| 76 |
+
"Bubble Sort", "Selection Sort", "Insertion Sort", "Merge Sort", "Quick Sort",
|
| 77 |
+
"Heap Sort", "Counting Sort","radix sort","bucket sort",
|
| 78 |
+
# Linked Lists
|
| 79 |
+
"Singly Linked List", "Doubly Linked List", "Circular Linked List",
|
| 80 |
+
# Stacks & Queues
|
| 81 |
+
"Stack", "Queue", "Circular Queue", "Deque", "Monotonic Stack",
|
| 82 |
+
# Trees
|
| 83 |
+
"Binary Tree", "Binary Search Tree", "AVL Tree", "Heap (Min/Max)", "Trie",
|
| 84 |
+
"Segment Tree", "Fenwick Tree (Binary Indexed Tree)",
|
| 85 |
+
# Graphs
|
| 86 |
+
"BFS", "DFS", "Dijkstra's Algorithm", "Bellman-Ford Algorithm",
|
| 87 |
+
"Floyd-Warshall Algorithm", "Kruskal's Algorithm (MST)", "Prim's Algorithm (MST)",
|
| 88 |
+
"Topological Sort", "Union-Find (Disjoint Set)",
|
| 89 |
+
# Dynamic Programming
|
| 90 |
+
"Dynamic Programming Overview", "0/1 Knapsack", "Longest Common Subsequence",
|
| 91 |
+
"Longest Increasing Subsequence", "Coin Change Problem", "Edit Distance",
|
| 92 |
+
# Greedy
|
| 93 |
+
"Greedy Algorithms Overview", "Activity Selection Problem", "Fractional Knapsack",
|
| 94 |
+
# Hashing
|
| 95 |
+
"Hash Table", "Hash Set vs Hash Map",
|
| 96 |
+
# Recursion & Backtracking
|
| 97 |
+
"Recursion Overview", "Backtracking Overview", "N-Queens Problem",
|
| 98 |
+
"Subset Sum / Power Set Generation",
|
| 99 |
+
# Bit Manipulation
|
| 100 |
+
"Bit Manipulation Basics", "XOR Tricks",
|
| 101 |
+
]
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def to_snake_case(topic: str) -> str:
|
| 105 |
+
"""Converts a topic name into a safe snake_case filename."""
|
| 106 |
+
cleaned = re.sub(r"[^\w\s-]", "", topic) # strip punctuation like ()/,'
|
| 107 |
+
cleaned = re.sub(r"\s+", "_", cleaned.strip()) # spaces -> underscores
|
| 108 |
+
return cleaned.lower()
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def generate_entry(topic: str) -> str:
|
| 112 |
+
prompt = PROMPT_TEMPLATE.format(topic=topic)
|
| 113 |
+
response = client.chat.completions.create(
|
| 114 |
+
model=MODEL,
|
| 115 |
+
messages=[{"role": "user", "content": prompt}],
|
| 116 |
+
temperature=0.4,
|
| 117 |
+
max_tokens=1024,
|
| 118 |
+
)
|
| 119 |
+
return response.choices[0].message.content.strip()
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def main():
|
| 123 |
+
print(f"Generating {len(TOPICS)} knowledge base files into ./{OUTPUT_DIR}/\n")
|
| 124 |
+
|
| 125 |
+
for i, topic in enumerate(TOPICS, start=1):
|
| 126 |
+
filename = to_snake_case(topic) + ".txt"
|
| 127 |
+
filepath = os.path.join(OUTPUT_DIR, filename)
|
| 128 |
+
|
| 129 |
+
if os.path.exists(filepath):
|
| 130 |
+
print(f"[{i}/{len(TOPICS)}] SKIP (already exists): {filename}")
|
| 131 |
+
continue
|
| 132 |
+
|
| 133 |
+
print(f"[{i}/{len(TOPICS)}] Generating: {topic} -> {filename}")
|
| 134 |
+
try:
|
| 135 |
+
content = generate_entry(topic)
|
| 136 |
+
with open(filepath, "w", encoding="utf-8") as f:
|
| 137 |
+
f.write(content)
|
| 138 |
+
except Exception as e:
|
| 139 |
+
print(f" ERROR generating '{topic}': {e}")
|
| 140 |
+
|
| 141 |
+
time.sleep(DELAY_BETWEEN_CALLS)
|
| 142 |
+
|
| 143 |
+
print("\nDone. Review files in ./knowledge_base/ before indexing into ChromaDB.")
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
if __name__ == "__main__":
|
| 147 |
+
main()
|
rag/knowledge_base/01_knapsack.txt
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TOPIC: 0/1 Knapsack
|
| 2 |
+
|
| 3 |
+
DEFINITION:
|
| 4 |
+
The 0/1 Knapsack problem is a classic problem in computer science where you have a set of items, each with a weight and a value, and you need to determine the subset of items to include in a knapsack of limited capacity such that the total value is maximized. This problem solves the issue of optimizing the selection of items when resources are limited. The "0/1" refers to the fact that each item is either included or excluded from the knapsack, with no partial inclusions allowed.
|
| 5 |
+
|
| 6 |
+
TIME_COMPLEXITY:
|
| 7 |
+
The time complexity of the 0/1 Knapsack problem is O(nW), where n is the number of items and W is the capacity of the knapsack. This is because the problem is typically solved using dynamic programming, which involves filling up a 2D table of size n x W.
|
| 8 |
+
|
| 9 |
+
SPACE_COMPLEXITY:
|
| 10 |
+
The space complexity of the 0/1 Knapsack problem is O(nW), which is used to store the 2D table that keeps track of the maximum value that can be obtained with a given capacity and number of items.
|
| 11 |
+
|
| 12 |
+
USE_WHEN:
|
| 13 |
+
The 0/1 Knapsack problem is useful when you need to make a selection from a set of items, each with its own value and cost, and you have limited resources. This problem is particularly relevant in scenarios such as budgeting, resource allocation, and investment decisions.
|
| 14 |
+
|
| 15 |
+
AVOID_WHEN:
|
| 16 |
+
You should avoid using the 0/1 Knapsack problem when the items can be divided or when the cost of an item is not directly proportional to its value, in which case other problems like the Fractional Knapsack problem may be more suitable.
|
| 17 |
+
|
| 18 |
+
EXAMPLE:
|
| 19 |
+
Suppose we have a knapsack with a capacity of 5 units and the following items:
|
| 20 |
+
- Item A: weight 2, value 10
|
| 21 |
+
- Item B: weight 3, value 8
|
| 22 |
+
- Item C: weight 1, value 5
|
| 23 |
+
We can solve this problem using dynamic programming as follows:
|
| 24 |
+
- Initialize a table with zeros:
|
| 25 |
+
[0, 0, 0, 0, 0, 0]
|
| 26 |
+
- Fill up the table:
|
| 27 |
+
- For Item A: [0, 0, 10, 10, 10, 10]
|
| 28 |
+
- For Item B: [0, 0, 10, 10, 10, 18]
|
| 29 |
+
- For Item C: [0, 5, 10, 15, 15, 18]
|
| 30 |
+
The maximum value we can get is 18, which is obtained by including Item B and Item C:
|
| 31 |
+
|
| 32 |
+
CHECKMARK:
|
| 33 |
+
|
| 34 |
+
REAL_WORLD_ANALOGY:
|
| 35 |
+
The 0/1 Knapsack problem is similar to going on a trip and having to pack a suitcase with limited space, where you need to decide which items to bring and which to leave behind in order to maximize your overall satisfaction.
|
| 36 |
+
|
| 37 |
+
SOURCE_NOTE:
|
rag/knowledge_base/activity_selection_problem.txt
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TOPIC: Activity Selection Problem
|
| 2 |
+
|
| 3 |
+
DEFINITION: The Activity Selection Problem is a classic problem in computer science and operations research that involves selecting the maximum number of activities that can be performed by a single person or resource, given a set of activities with start and end times. The goal is to choose the activities that do not conflict with each other, meaning that the end time of one activity is less than or equal to the start time of the next activity. This problem is useful in a variety of real-world scenarios, such as scheduling meetings or tasks.
|
| 4 |
+
|
| 5 |
+
TIME_COMPLEXITY: O(n log n) due to the sorting of activities by their end times, which is necessary to apply the greedy algorithm that solves the problem.
|
| 6 |
+
|
| 7 |
+
SPACE_COMPLEXITY: O(n) for storing the list of activities, where n is the number of activities.
|
| 8 |
+
|
| 9 |
+
USE_WHEN: This problem is the right tool when you need to schedule a set of tasks or activities that have specific start and end times, and you want to maximize the number of activities that can be performed without any conflicts. It is particularly useful when the activities are mutually exclusive, meaning that only one activity can be performed at a time.
|
| 10 |
+
|
| 11 |
+
AVOID_WHEN: This problem is a poor choice when the activities are not mutually exclusive, or when there are additional constraints that need to be considered, such as resource availability or priority levels. In such cases, more advanced scheduling algorithms or techniques, such as dynamic programming or integer programming, may be more suitable.
|
| 12 |
+
|
| 13 |
+
EXAMPLE:
|
| 14 |
+
Consider the following set of activities with their start and end times:
|
| 15 |
+
[1] (1, 4)
|
| 16 |
+
[2] (3, 5)
|
| 17 |
+
[3] (0, 6)
|
| 18 |
+
[4] (5, 7)
|
| 19 |
+
[5] (3, 8)
|
| 20 |
+
[6] (5, 9)
|
| 21 |
+
[7] (6, 10)
|
| 22 |
+
First, sort the activities by their end times:
|
| 23 |
+
[1] (1, 4)
|
| 24 |
+
[2] (3, 5)
|
| 25 |
+
[4] (5, 7)
|
| 26 |
+
[7] (6, 10)
|
| 27 |
+
[5] (3, 8)
|
| 28 |
+
[6] (5, 9)
|
| 29 |
+
[3] (0, 6)
|
| 30 |
+
Then, apply the greedy algorithm:
|
| 31 |
+
Select [1] (1, 4)
|
| 32 |
+
Select [4] (5, 7)
|
| 33 |
+
Select [7] (6, 10) is not selected because it conflicts with [4]
|
| 34 |
+
The final selected activities are:
|
| 35 |
+
[1] (1, 4)
|
| 36 |
+
[4] (5, 7)
|
| 37 |
+
Checkmark: The maximum number of non-conflicting activities is 2.
|
| 38 |
+
|
| 39 |
+
REAL_WORLD_ANALOGY: The Activity Selection Problem is similar to scheduling meetings in a conference room, where you want to maximize the number of meetings that can be held without any conflicts or overlaps.
|
| 40 |
+
|
| 41 |
+
SOURCE_NOTE: Concepts referenced from general knowledge of greedy algorithms and scheduling problems.
|
rag/knowledge_base/avl_tree.txt
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TOPIC: AVL Tree
|
| 2 |
+
|
| 3 |
+
DEFINITION:
|
| 4 |
+
An AVL tree is a self-balancing binary search tree that ensures the height of the tree remains relatively small by rotating nodes when the balance factor becomes too large, thus solving the problem of maintaining efficient search, insertion, and deletion operations. This balance is crucial for preventing the tree from becoming skewed, which would lead to reduced performance.
|
| 5 |
+
|
| 6 |
+
TIME_COMPLEXITY:
|
| 7 |
+
The time complexity for search, insertion, and deletion in an AVL tree is O(log n) in all cases, as the tree's height is kept logarithmic by the self-balancing mechanism.
|
| 8 |
+
|
| 9 |
+
SPACE_COMPLEXITY:
|
| 10 |
+
The space complexity of an AVL tree is O(n), where n is the number of nodes in the tree, as each node requires a constant amount of space to store its value, child pointers, and balance factor.
|
| 11 |
+
|
| 12 |
+
USE_WHEN:
|
| 13 |
+
Use an AVL tree when you need to frequently insert, delete, and search for elements in a dataset while maintaining an efficient order, such as in database indexing or file systems. This data structure is particularly useful when the dataset is constantly changing.
|
| 14 |
+
|
| 15 |
+
AVOID_WHEN:
|
| 16 |
+
Avoid using an AVL tree when the dataset is mostly static or when memory is extremely limited, as the overhead of maintaining the balance factor and performing rotations can be unnecessary or too costly; in such cases, a simpler binary search tree or even an array might be more suitable.
|
| 17 |
+
|
| 18 |
+
EXAMPLE:
|
| 19 |
+
Let's insert elements 5, 2, 8, 3, 9 into an AVL tree step by step:
|
| 20 |
+
- Start with an empty tree.
|
| 21 |
+
- Insert 5: [5]
|
| 22 |
+
- Insert 2: [5]
|
| 23 |
+
/
|
| 24 |
+
[2]
|
| 25 |
+
- Insert 8: [5]
|
| 26 |
+
/ \
|
| 27 |
+
[2] [8]
|
| 28 |
+
- Insert 3: Since 3 is between 2 and 5, we insert it as the right child of 2.
|
| 29 |
+
[5]
|
| 30 |
+
/ \
|
| 31 |
+
[2] [8]
|
| 32 |
+
\
|
| 33 |
+
[3]
|
| 34 |
+
- Insert 9: The tree becomes unbalanced, so we rotate:
|
| 35 |
+
[5]
|
| 36 |
+
/ \
|
| 37 |
+
[2] [8]
|
| 38 |
+
\ \
|
| 39 |
+
[3] [9]
|
| 40 |
+
After rotation:
|
| 41 |
+
[5]
|
| 42 |
+
/ \
|
| 43 |
+
[2] [8]
|
| 44 |
+
\ / \
|
| 45 |
+
[3] [9]
|
| 46 |
+
Final balanced tree:
|
| 47 |
+
[5]
|
| 48 |
+
/ \
|
| 49 |
+
[2] [8]
|
| 50 |
+
\ \
|
| 51 |
+
[3] [9]
|
| 52 |
+
Result:
|
| 53 |
+
The final balanced AVL tree is:
|
| 54 |
+
[5]
|
| 55 |
+
/ \
|
| 56 |
+
[2] [8]
|
| 57 |
+
\ \
|
| 58 |
+
[3] [9]
|
| 59 |
+
|
| 60 |
+
REAL_WORLD_ANALOGY:
|
| 61 |
+
An AVL tree can be thought of like a librarian who constantly rearranges books on shelves to ensure that the shelves remain balanced and it's easy to find any book, similar to how the tree balances itself to maintain efficient data retrieval.
|
| 62 |
+
|
| 63 |
+
SOURCE_NOTE:
|
rag/knowledge_base/backtracking_overview.txt
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TOPIC: Backtracking Overview
|
| 2 |
+
|
| 3 |
+
DEFINITION:
|
| 4 |
+
Backtracking is a problem-solving strategy used in algorithms to find a solution by exploring all possible options in a systematic way. It involves recursively trying different paths until a valid solution is found, and if a path does not lead to a solution, it backtracks to the previous step and tries another path. This approach is particularly useful for solving constraint satisfaction problems.
|
| 5 |
+
|
| 6 |
+
TIME_COMPLEXITY:
|
| 7 |
+
The time complexity of backtracking algorithms can vary, but in the worst case, it is typically O(n!), where n is the number of options to choose from, because it may need to explore all possible combinations. The average and best cases depend on the specific problem and the order in which options are explored.
|
| 8 |
+
|
| 9 |
+
SPACE_COMPLEXITY:
|
| 10 |
+
The space complexity of backtracking algorithms is usually O(n), where n is the maximum depth of the recursion tree, because that's how much space is needed to store the current path being explored.
|
| 11 |
+
|
| 12 |
+
USE_WHEN:
|
| 13 |
+
Backtracking is the right tool to use when dealing with problems that have multiple possible solutions and require exploring all options in a systematic way, such as puzzles, scheduling, or resource allocation problems. It's particularly useful when the problem can be broken down into smaller sub-problems and solved recursively.
|
| 14 |
+
|
| 15 |
+
AVOID_WHEN:
|
| 16 |
+
Backtracking is a poor choice for problems that have a very large solution space or require finding the optimal solution quickly, because it can be slow and inefficient. In such cases, other algorithms like dynamic programming or greedy algorithms may be more suitable.
|
| 17 |
+
|
| 18 |
+
EXAMPLE:
|
| 19 |
+
Suppose we want to find all possible ways to arrange the letters A, B, and C. We can use backtracking to solve this problem:
|
| 20 |
+
Start with an empty string: []
|
| 21 |
+
Try adding A: [A]
|
| 22 |
+
Try adding B: [A, B]
|
| 23 |
+
Try adding C: [A, B, C]
|
| 24 |
+
Try adding C: [A, C]
|
| 25 |
+
Try adding B: [A, C, B]
|
| 26 |
+
Try adding B: [B]
|
| 27 |
+
Try adding A: [B, A]
|
| 28 |
+
Try adding C: [B, A, C]
|
| 29 |
+
Try adding C: [B, C]
|
| 30 |
+
Try adding A: [B, C, A]
|
| 31 |
+
Try adding C: [C]
|
| 32 |
+
Try adding A: [C, A]
|
| 33 |
+
Try adding B: [C, A, B]
|
| 34 |
+
Try adding B: [C, B]
|
| 35 |
+
Try adding A: [C, B, A]
|
| 36 |
+
The final solutions are: [A, B, C] , [A, C, B] , [B, A, C] , [B, C, A] , [C, A, B] , [C, B, A]
|
| 37 |
+
|
| 38 |
+
REAL_WORLD_ANALOGY:
|
| 39 |
+
Backtracking is similar to trying different routes to reach a destination, where you explore each path until you find the one that leads you to your goal, and if a path doesn't work out, you go back to the previous intersection and try another route.
|
| 40 |
+
|
| 41 |
+
SOURCE_NOTE:
|
| 42 |
+
Concepts referenced from general knowledge of algorithms and problem-solving strategies.
|
rag/knowledge_base/bellman-ford_algorithm.txt
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TOPIC: Bellman-Ford Algorithm
|
| 2 |
+
|
| 3 |
+
DEFINITION: The Bellman-Ford algorithm is a graph search algorithm that finds the shortest path from a source vertex to all other vertices in a weighted graph. It can handle negative weight edges and can detect negative weight cycles, making it a versatile tool for solving various graph-related problems. This algorithm is particularly useful for finding the shortest path in graphs where the edge weights can be negative.
|
| 4 |
+
|
| 5 |
+
TIME_COMPLEXITY: The time complexity of the Bellman-Ford algorithm is O(VE), where V is the number of vertices and E is the number of edges, because it relaxes all edges V-1 times.
|
| 6 |
+
|
| 7 |
+
SPACE_COMPLEXITY: The space complexity is O(V), as the algorithm needs to store the distance to each vertex from the source vertex.
|
| 8 |
+
|
| 9 |
+
USE_WHEN: The Bellman-Ford algorithm is the right tool when you need to find the shortest path in a graph with negative weight edges, or when you need to detect negative weight cycles. It's particularly useful in scenarios where other algorithms like Dijkstra's may fail due to negative weights.
|
| 10 |
+
|
| 11 |
+
AVOID_WHEN: You should avoid using the Bellman-Ford algorithm when the graph is very large and dense, as its O(VE) time complexity can be slow, and in such cases, more efficient algorithms like Dijkstra's or A* may be more suitable for graphs with non-negative weights.
|
| 12 |
+
|
| 13 |
+
EXAMPLE:
|
| 14 |
+
Let's consider a graph with 4 vertices (A, B, C, D) and 5 edges:
|
| 15 |
+
A -> B (weight: -1)
|
| 16 |
+
A -> C (weight: 4)
|
| 17 |
+
B -> C (weight: 3)
|
| 18 |
+
B -> D (weight: 2)
|
| 19 |
+
C -> D (weight: 2)
|
| 20 |
+
Initially, the distance to all vertices is infinity, except for A which is 0.
|
| 21 |
+
A: 0
|
| 22 |
+
B: inf
|
| 23 |
+
C: inf
|
| 24 |
+
D: inf
|
| 25 |
+
After the first iteration:
|
| 26 |
+
A: 0
|
| 27 |
+
B: -1
|
| 28 |
+
C: inf
|
| 29 |
+
D: inf
|
| 30 |
+
After the second iteration:
|
| 31 |
+
A: 0
|
| 32 |
+
B: -1
|
| 33 |
+
C: 2
|
| 34 |
+
D: inf
|
| 35 |
+
After the third iteration:
|
| 36 |
+
A: 0
|
| 37 |
+
B: -1
|
| 38 |
+
C: 2
|
| 39 |
+
D: 1
|
| 40 |
+
After the fourth iteration, no changes occur, so we stop.
|
| 41 |
+
The shortest distances are:
|
| 42 |
+
A to B: -1
|
| 43 |
+
A to C: 2
|
| 44 |
+
A to D: 1
|
| 45 |
+
Checkmark: The shortest path from A to D is A -> B -> D with a total weight of 1.
|
| 46 |
+
|
| 47 |
+
REAL_WORLD_ANALOGY: The Bellman-Ford algorithm can be thought of as finding the cheapest route to travel between cities, where the cost of traveling between cities can sometimes be negative, such as getting a refund for a part of the journey.
|
| 48 |
+
|
| 49 |
+
SOURCE_NOTE: Concepts referenced from general knowledge of graph algorithms and data structures.
|
rag/knowledge_base/bfs.txt
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TOPIC: BFS
|
| 2 |
+
|
| 3 |
+
DEFINITION:
|
| 4 |
+
Breadth-First Search (BFS) is a graph traversal algorithm that explores all the nodes at a given depth level before moving on to the next level, solving the problem of finding the shortest path between two nodes in an unweighted graph. It's particularly useful for searching graphs or trees level by level, starting from a given source node. This approach ensures that the algorithm visits all the nodes closest to the source before moving further away.
|
| 5 |
+
|
| 6 |
+
TIME_COMPLEXITY:
|
| 7 |
+
The time complexity of BFS is O(V + E), where V represents the number of vertices (nodes) and E represents the number of edges, because in the worst case, the algorithm visits every node and edge once.
|
| 8 |
+
|
| 9 |
+
SPACE_COMPLEXITY:
|
| 10 |
+
The space complexity of BFS is O(V), as the algorithm uses a queue to store nodes to be visited, and in the worst case, the queue can hold all the nodes at the deepest level.
|
| 11 |
+
|
| 12 |
+
USE_WHEN:
|
| 13 |
+
BFS is the right tool when you need to find the shortest path in an unweighted graph or when you want to traverse a graph level by level, such as in web crawlers or social network friend suggestions. It's particularly useful when the graph is very large and you want to avoid the overhead of more complex algorithms.
|
| 14 |
+
|
| 15 |
+
AVOID_WHEN:
|
| 16 |
+
BFS is a poor choice when dealing with very deep graphs or trees, as it can be slow and memory-intensive, and in such cases, a depth-first search (DFS) or more specialized algorithms like Dijkstra's or A* might be more suitable.
|
| 17 |
+
|
| 18 |
+
EXAMPLE:
|
| 19 |
+
Consider a graph with nodes A, B, C, D, and E, where A is connected to B and C, B is connected to D, and C is connected to E. A BFS traversal starting from A would visit the nodes in the following order:
|
| 20 |
+
A
|
| 21 |
+
-> B, C
|
| 22 |
+
-> D, E
|
| 23 |
+
The final result is a visited set {A, B, C, D, E}
|
| 24 |
+
|
| 25 |
+
REAL_WORLD_ANALOGY:
|
| 26 |
+
BFS can be thought of as exploring a city by first visiting all the neighboring houses, then moving on to the next block, and so on, similar to how a fire department might search a neighborhood.
|
| 27 |
+
|
| 28 |
+
SOURCE_NOTE:
|
| 29 |
+
Concepts referenced from general knowledge of graph traversal algorithms.
|
rag/knowledge_base/binary_search.txt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TOPIC: Binary Search
|
| 2 |
+
|
| 3 |
+
DEFINITION: Binary search is an efficient algorithm for finding an item from a sorted list of items. It works by repeatedly dividing in half the portion of the list that could contain the item, until you've narrowed the possible locations to just one. This approach solves the problem of quickly locating a specific element within a large sorted dataset.
|
| 4 |
+
|
| 5 |
+
TIME_COMPLEXITY: The time complexity of binary search is O(log n), where n is the number of items in the list. This is because with each comparison, the algorithm effectively halves the search space, leading to a logarithmic number of steps.
|
| 6 |
+
|
| 7 |
+
SPACE_COMPLEXITY: The space complexity is O(1), as binary search only requires a constant amount of additional space to store the indices and the target value, regardless of the size of the input list.
|
| 8 |
+
|
| 9 |
+
USE_WHEN: Use binary search when you need to find an item in a large sorted list, and speed is crucial. This is particularly useful in scenarios where the list is too large to search linearly, and the data is already sorted or can be sorted efficiently.
|
| 10 |
+
|
| 11 |
+
AVOID_WHEN: Avoid using binary search when the list is unsorted, as the algorithm relies on the data being sorted to work correctly. In such cases, a linear search or a different sorting algorithm followed by binary search might be more appropriate.
|
| 12 |
+
|
| 13 |
+
EXAMPLE:
|
| 14 |
+
Suppose we have a sorted list [2, 5, 8, 12, 16, 23] and we're looking for the number 12.
|
| 15 |
+
Start with the entire list: [2, 5, 8, 12, 16, 23]
|
| 16 |
+
-> Compare the middle element (8) with the target (12). Since 12 > 8, we know 12 must be in the right half.
|
| 17 |
+
Narrow down to the right half: [12, 16, 23]
|
| 18 |
+
-> Compare the middle element (16) with the target (12). Since 12 < 16, we know 12 must be in the left half of this subset.
|
| 19 |
+
Narrow down further: [12]
|
| 20 |
+
-> The only element left is 12, which matches our target:
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
REAL_WORLD_ANALOGY: Binary search is similar to finding a specific book in a library where the books are arranged alphabetically by title. You start by looking at the middle shelf, then move to the left or right half depending on whether the book you're looking for comes before or after the middle book alphabetically, repeating this process until you find the book.
|
| 24 |
+
|
| 25 |
+
SOURCE_NOTE:
|
rag/knowledge_base/binary_search_tree.txt
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TOPIC: Binary Search Tree
|
| 2 |
+
|
| 3 |
+
DEFINITION: A Binary Search Tree (BST) is a data structure in which each node has at most two children (i.e., left child and right child) and each node represents a value. This structure allows for efficient storage and retrieval of data by ensuring that all values to the left of a node are less than the node's value, and all values to the right are greater. This property enables fast searching, inserting, and deleting of nodes.
|
| 4 |
+
|
| 5 |
+
TIME_COMPLEXITY: The time complexity of a Binary Search Tree is O(log n) for best and average cases, and O(n) for the worst case, which occurs when the tree becomes a skewed tree (essentially a linked list).
|
| 6 |
+
|
| 7 |
+
SPACE_COMPLEXITY: The space complexity of a Binary Search Tree is O(n), where n is the number of nodes in the tree, as each node requires a constant amount of space to store its value and references to its children.
|
| 8 |
+
|
| 9 |
+
USE_WHEN: Use a Binary Search Tree when you need to store and retrieve large amounts of data efficiently, and the data needs to be ordered or searchable. This is particularly useful in scenarios where data is constantly being inserted or deleted.
|
| 10 |
+
|
| 11 |
+
AVOID_WHEN: Avoid using a Binary Search Tree when the data is mostly static or when the tree may become heavily unbalanced, as this can lead to poor performance; in such cases, a self-balancing binary search tree like an AVL tree or a Red-Black tree may be more suitable.
|
| 12 |
+
|
| 13 |
+
EXAMPLE:
|
| 14 |
+
Suppose we have a BST with the following nodes:
|
| 15 |
+
5
|
| 16 |
+
/ \
|
| 17 |
+
3 7
|
| 18 |
+
/ \ \
|
| 19 |
+
2 4 8
|
| 20 |
+
We want to insert the value 6.
|
| 21 |
+
First, we start at the root node (5) and compare it to the value we want to insert (6).
|
| 22 |
+
Since 6 is greater than 5, we move to the right child (7).
|
| 23 |
+
Then, we compare 6 to 7; since 6 is less than 7, we move to the left child (which doesn't exist),
|
| 24 |
+
so we create a new node with the value 6 as the left child of 7.
|
| 25 |
+
The resulting tree is:
|
| 26 |
+
5
|
| 27 |
+
/ \
|
| 28 |
+
3 7
|
| 29 |
+
/ \ / \
|
| 30 |
+
2 4 6 8
|
| 31 |
+
The insertion is successful
|
| 32 |
+
|
| 33 |
+
REAL_WORLD_ANALOGY: A Binary Search Tree can be thought of as a phonebook where each entry is a person's name, and the names are arranged alphabetically; when you look up a name, you can quickly find it by comparing it to the names on either side of the current page.
|
| 34 |
+
|
| 35 |
+
SOURCE_NOTE: Concepts referenced from general knowledge of data structures and algorithms.
|
rag/knowledge_base/binary_tree.txt
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TOPIC: Binary Tree
|
| 2 |
+
|
| 3 |
+
DEFINITION:
|
| 4 |
+
A binary tree is a data structure in which each node has at most two children, referred to as the left child and the right child. This structure allows for efficient storage and retrieval of data, particularly when the data has a hierarchical relationship. It solves the problem of organizing and searching large amounts of data by providing a systematic way to traverse and access the data.
|
| 5 |
+
|
| 6 |
+
TIME_COMPLEXITY:
|
| 7 |
+
The time complexity of binary tree operations such as search, insert, and delete can vary: O(1) for the best case when the tree is balanced and the operation is performed at the root, O(log n) for the average case when the tree is balanced, and O(n) for the worst case when the tree is skewed.
|
| 8 |
+
|
| 9 |
+
SPACE_COMPLEXITY:
|
| 10 |
+
The space complexity of a binary tree is O(n), where n is the number of nodes in the tree, since each node requires a constant amount of space to store its value and references to its children.
|
| 11 |
+
|
| 12 |
+
USE_WHEN:
|
| 13 |
+
Use a binary tree when you need to store and retrieve data efficiently, especially when the data has a natural hierarchical structure, such as a file system or a database index. It's also useful when you need to perform operations like insertion, deletion, and search frequently.
|
| 14 |
+
|
| 15 |
+
AVOID_WHEN:
|
| 16 |
+
Avoid using a binary tree when the data is too large to fit into memory or when the tree becomes severely unbalanced, leading to poor performance. In such cases, consider using other data structures like hash tables or self-balancing trees like AVL or Red-Black trees.
|
| 17 |
+
|
| 18 |
+
EXAMPLE:
|
| 19 |
+
Suppose we have a binary tree with the following nodes:
|
| 20 |
+
4
|
| 21 |
+
/ \
|
| 22 |
+
2 6
|
| 23 |
+
/ \ \
|
| 24 |
+
1 3 5
|
| 25 |
+
To insert a new node with value 7, we start at the root (4) and move to the right child (6), then to the right child again (which doesn't exist), so we create a new node (7) as the right child of 6:
|
| 26 |
+
4
|
| 27 |
+
/ \
|
| 28 |
+
2 6
|
| 29 |
+
/ \ / \
|
| 30 |
+
1 3 5 7
|
| 31 |
+
The resulting tree is now:
|
| 32 |
+
4
|
| 33 |
+
/ \
|
| 34 |
+
2 6
|
| 35 |
+
/ \ / \
|
| 36 |
+
1 3 5 7
|
| 37 |
+
Result:
|
| 38 |
+
|
| 39 |
+
REAL_WORLD_ANALOGY:
|
| 40 |
+
A binary tree can be thought of as a family tree, where each person (node) has at most two children, and the relationships between family members are organized in a hierarchical structure.
|
| 41 |
+
|
| 42 |
+
SOURCE_NOTE:
|
| 43 |
+
Concepts referenced from general knowledge of data structures and algorithms.
|
rag/knowledge_base/bit_manipulation_basics.txt
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TOPIC: Bit Manipulation Basics
|
| 2 |
+
|
| 3 |
+
DEFINITION: Bit manipulation is a technique used to perform operations on the binary representation of numbers, allowing for efficient and low-level control over data. It solves problems that require direct access and modification of individual bits or groups of bits within a binary number. This is particularly useful in situations where memory or computational resources are limited.
|
| 4 |
+
|
| 5 |
+
TIME_COMPLEXITY: O(1) - Bit manipulation operations are typically constant time because they involve simple bitwise operations that do not depend on the size of the input.
|
| 6 |
+
|
| 7 |
+
SPACE_COMPLEXITY: O(1) - The space complexity is constant because bit manipulation operations usually only require a small, fixed amount of space to store the input and output values.
|
| 8 |
+
|
| 9 |
+
USE_WHEN: Bit manipulation is the right tool when working with low-level data representations, such as flags, masks, or bit fields, and when performing operations like setting, clearing, or toggling individual bits. It is also useful when optimizing code for performance or memory usage.
|
| 10 |
+
|
| 11 |
+
AVOID_WHEN: Bit manipulation is a poor choice when working with high-level abstractions or complex data structures, and when readability and maintainability are more important than performance. In such cases, using higher-level constructs and libraries may be more appropriate.
|
| 12 |
+
|
| 13 |
+
EXAMPLE:
|
| 14 |
+
Start with the binary number: 1010
|
| 15 |
+
Apply a bitwise AND operation with the mask: 1100
|
| 16 |
+
1010
|
| 17 |
+
& 1100
|
| 18 |
+
----
|
| 19 |
+
1000
|
| 20 |
+
The result is: 1000
|
| 21 |
+
|
| 22 |
+
REAL_WORLD_ANALOGY: Bit manipulation is similar to working with a combination lock, where each bit represents a specific switch that can be turned on or off to achieve a desired outcome.
|
| 23 |
+
|
| 24 |
+
SOURCE_NOTE: Concepts referenced from general knowledge of computer science and programming principles.
|
rag/knowledge_base/bubble_sort.txt
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TOPIC: Bubble Sort
|
| 2 |
+
|
| 3 |
+
DEFINITION:
|
| 4 |
+
Bubble sort is a simple sorting algorithm that repeatedly steps through the list, compares adjacent elements, and swaps them if they are in the wrong order. This process is repeated until the list is sorted, solving the problem of arranging elements in ascending or descending order. It works by iteratively "bubbling" the largest or smallest element to the end or beginning of the list.
|
| 5 |
+
|
| 6 |
+
TIME_COMPLEXITY:
|
| 7 |
+
The time complexity of bubble sort is O(n) in the best case, O(n^2) on average, and O(n^2) in the worst case, because in the best case the list is already sorted and only one pass is needed, while in the average and worst cases, the algorithm needs to make multiple passes through the list to sort it.
|
| 8 |
+
|
| 9 |
+
SPACE_COMPLEXITY:
|
| 10 |
+
The space complexity of bubble sort is O(1), because it only uses a constant amount of additional space to store temporary swaps, and does not require any extra space that scales with the size of the input list.
|
| 11 |
+
|
| 12 |
+
USE_WHEN:
|
| 13 |
+
Bubble sort can be useful when the list is small or nearly sorted, as it has a simple implementation and can be efficient in these cases. It can also be a good teaching tool for introducing the concept of sorting algorithms.
|
| 14 |
+
|
| 15 |
+
AVOID_WHEN:
|
| 16 |
+
Bubble sort is a poor choice for large lists or lists that are in reverse order, as its average and worst-case time complexities are high, and other algorithms like quicksort or mergesort are generally more efficient and should be used instead.
|
| 17 |
+
|
| 18 |
+
EXAMPLE:
|
| 19 |
+
Suppose we have the list [5, 2, 8, 3, 1] and we want to sort it in ascending order using bubble sort.
|
| 20 |
+
Initial list: [5, 2, 8, 3, 1]
|
| 21 |
+
Compare 5 and 2, swap: [2, 5, 8, 3, 1]
|
| 22 |
+
Compare 5 and 8, no swap: [2, 5, 8, 3, 1]
|
| 23 |
+
Compare 8 and 3, swap: [2, 5, 3, 8, 1]
|
| 24 |
+
Compare 8 and 1, swap: [2, 5, 3, 1, 8]
|
| 25 |
+
Repeat the process until the list is sorted:
|
| 26 |
+
[2, 5, 3, 1, 8] -> [2, 3, 5, 1, 8] -> [2, 3, 1, 5, 8] -> [2, 1, 3, 5, 8] -> [1, 2, 3, 5, 8]
|
| 27 |
+
Final sorted list: [1, 2, 3, 5, 8]
|
| 28 |
+
|
| 29 |
+
REAL_WORLD_ANALOGY:
|
| 30 |
+
Bubble sort is similar to rearranging a set of books on a shelf, where you compare each book to the one next to it and swap them if they are in the wrong order, repeating the process until the books are in the correct order.
|
| 31 |
+
|
| 32 |
+
SOURCE_NOTE:
|
| 33 |
+
Concepts referenced from general knowledge of sorting algorithms and data structures.
|