Add knowledge base RAG module for book Q&A

- Create knowledge.py handler with dynamic book selection
- Support list/select/query actions for multiple books
- Implement vector search with cosine similarity
- Add knowledge detection to AI parser config
- Cache embeddings per-book for performance
This commit is contained in:
2026-02-16 11:20:22 -06:00
parent 362716e093
commit 0f270dbba2
3 changed files with 326 additions and 21 deletions

View File

@@ -20,8 +20,9 @@ import pickle
from bot.command_registry import get_handler, list_registered
import ai.parser as ai_parser
import bot.commands.routines # noqa: F401 - registers handler
import bot.commands.routines # noqa: F401 - registers handler
import bot.commands.medications # noqa: F401 - registers handler
import bot.commands.knowledge # noqa: F401 - registers handler
DISCORD_BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN")
API_URL = os.getenv("API_URL", "http://app:5000")
@@ -217,7 +218,7 @@ async def checkActiveSession(session):
token = session.get("token")
if not token:
return None
resp, status = apiRequest("get", "/api/sessions/active", token)
if status == 200 and "session" in resp:
return resp
@@ -228,45 +229,45 @@ async def handleConfirmation(message, session):
"""Handle yes/no confirmation responses. Returns True if handled."""
discord_id = message.author.id
user_input = message.content.lower().strip()
if "pending_confirmations" not in session:
return False
# Check for any pending confirmations
pending = session["pending_confirmations"]
if not pending:
return False
# Get the most recent pending confirmation
confirmation_id = list(pending.keys())[-1]
confirmation_data = pending[confirmation_id]
if user_input in ("yes", "y", "yeah", "sure", "ok", "confirm"):
# Execute the confirmed action
del pending[confirmation_id]
interaction_type = confirmation_data.get("interaction_type")
handler = get_handler(interaction_type)
if handler:
# Create a fake parsed object for the handler
fake_parsed = confirmation_data.copy()
fake_parsed["needs_confirmation"] = False
await handler(message, session, fake_parsed)
return True
elif user_input in ("no", "n", "nah", "cancel", "abort"):
del pending[confirmation_id]
await message.channel.send("❌ Cancelled.")
return True
return False
async def handleActiveSessionShortcuts(message, session, active_session):
"""Handle shortcuts like 'done', 'skip', 'next' when in active session."""
user_input = message.content.lower().strip()
# Map common shortcuts to actions
shortcuts = {
"done": ("routine", "complete"),
@@ -283,7 +284,7 @@ async def handleActiveSessionShortcuts(message, session, active_session):
"quit": ("routine", "cancel"),
"abort": ("routine", "abort"),
}
if user_input in shortcuts:
interaction_type, action = shortcuts[user_input]
handler = get_handler(interaction_type)
@@ -291,7 +292,7 @@ async def handleActiveSessionShortcuts(message, session, active_session):
fake_parsed = {"action": action}
await handler(message, session, fake_parsed)
return True
return False
@@ -306,21 +307,23 @@ async def routeCommand(message):
# Check for active session first
active_session = await checkActiveSession(session)
# Handle confirmation responses
confirmation_handled = await handleConfirmation(message, session)
if confirmation_handled:
return
# Handle shortcuts when in active session
if active_session:
shortcut_handled = await handleActiveSessionShortcuts(message, session, active_session)
shortcut_handled = await handleActiveSessionShortcuts(
message, session, active_session
)
if shortcut_handled:
return
async with message.channel.typing():
history = message_history.get(discord_id, [])
# Add context about active session to help AI understand
context = ""
if active_session:
@@ -329,8 +332,10 @@ async def routeCommand(message):
current_step = session_data.get("current_step_index", 0) + 1
total_steps = active_session.get("total_steps", 0)
context = f"\n[Context: User is currently in active session for '{routine_name}', on step {current_step} of {total_steps}. They can say 'done', 'skip', 'pause', 'resume', or 'stop'.]"
parsed = ai_parser.parse(message.content + context, "command_parser", history=history)
parsed = ai_parser.parse(
message.content + context, "command_parser", history=history
)
if discord_id not in message_history:
message_history[discord_id] = []