improve web search depth and results, stop AI from hedging on known answers

This commit is contained in:
Justin Oro
2026-09-09 18:35:55 -07:00
parent c79278ab0b
commit 6c70f14359
4 changed files with 37 additions and 30 deletions
+2 -2
View File
@@ -2,7 +2,7 @@
A Discord bot for your server. Type `!watch GAME NAME` in any channel to start tracking a PC game's price, `!remove GAME NAME` (or `!unwatch GAME NAME`) to stop, `!list` to see what is being watched, `!price GAME NAME` to check the best price without watching it, `!prices GAME NAME` to list every store it's on sale at, cheapest first, `!history GAME NAME` to see every recorded sale over the past 12 months with date and price, `!predict GAME NAME` to guess when it might go on sale next, `!info GAME NAME` to see its platforms, multiplayer/co-op/cross-play info, a link to its store page, and how many people on the server own it, `!own GAME NAME` to mark a game as owned by you, `!unown GAME NAME` to remove it from your owned list, `!my-steam-profile LINK` to link your Steam profile so `!info` can count games you own automatically, `!who GAME NAME` to list who has confirmed they own a game, `!popular` to list games owned by more than one member, most owned first, `!remember SOMETHING` to permanently teach it a fact that survives restarts and is shared with everyone it talks to, `!forget SOMETHING` (or `!forget all`) to make it forget something you had it remember, `!memories` to see everything it remembers about you, `!check` to run the sale check on demand, `!shops` to see which stores it covers. Once a day it checks the best current price across PC stores for everything watched and posts in the channel it was added from when something is on sale, with the title, sale price, and which store it is at.
Mention the bot's trigger word anywhere in a message (that doesn't start with `!`) and, if Ollama is installed and running (and `AI_ENABLED` isn't turned off), it thinks up a short, in-character answer to whatever you said using a free AI model running right on your own computer - no API key, no cost, nothing sent anywhere over the internet. Without Ollama running (or with `AI_ENABLED=false`), it doesn't reply to mentions at all. It never fires on `!` commands, even ones that happen to contain the trigger word. It also handles general video game questions this way (item locations, boss strategies, release dates, and so on), not just questions about the bot itself - see `BRAVE_API_KEY` below for making those more accurate. The bot's name, trigger word, and personality are entirely up to you - edit `personality.yaml` to set them (see [Notes](#notes)); it ships pre-filled as BMO from Adventure Time as an example.
Mention the bot's trigger word anywhere in a message (that doesn't start with `!`) and, if Ollama is installed and running (and `AI_ENABLED` isn't turned off), it thinks up a short, in-character answer to whatever you said using a free AI model running right on your own computer - no API key, no cost, nothing sent anywhere over the internet. Without Ollama running (or with `AI_ENABLED=false`), it doesn't reply to mentions at all. It never fires on `!` commands, even ones that happen to contain the trigger word. It also handles general video game questions this way (item locations, boss strategies, release dates, and so on), not just questions about the bot itself - see `TAVILY_API_KEY` below for making those more accurate. The bot's name, trigger word, and personality are entirely up to you - edit `personality.yaml` to set them (see [Notes](#notes)); it ships pre-filled as BMO from Adventure Time as an example.
When someone new joins the server, it posts a random greeting from `greetings.yaml` in your #general channel. When someone leaves, it posts a random goodbye from `goodbyes.yaml` there too.
@@ -86,7 +86,7 @@ To change the token or key later, edit `.env` directly, then run the installer a
- The bot only replies to mentions of its trigger word if Ollama is installed, running, and has the model in `OLLAMA_MODEL` downloaded - without that, mentioning it gets no reply at all, and nothing is logged (check `bot.error.log` if you expect it to be working and it isn't replying). Each person can only trigger an AI answer once every 5 seconds; asking again sooner (or the Ollama request failing) just gets no reply rather than a second request, which keeps a burst of questions from bogging down your computer generating several replies at once.
- `!remember SOMETHING` teaches it a permanent fact - a name, a preference, a correction like "don't call me buddy", or something about another member mentioned by name - that it will always take into account, even after the bot restarts. These facts are shared: anyone can ask about a fact someone else taught it, not just the person who typed `!remember`. They're saved to `memory.yaml` under whoever typed the command, and never expire on their own, unlike the short-term memory described below. `!forget SOMETHING` removes one you personally taught it (it has to match what you typed to `!remember`, word for word - use `!memories` to see the exact wording it has stored under your name), and `!forget all` clears everything you've had it remember. Each person can have up to 20 things remembered at a time.
- `OLLAMA_MODEL` in `.env` controls which model it uses (`llama3.2` by default - small, fast, and good enough for short in-character replies). To use a different one, pull it yourself with `ollama pull MODEL_NAME`, set `OLLAMA_MODEL=MODEL_NAME` in `.env`, and restart it (re-run the installer, or `pm2 restart discord-bot`). Bigger models give better answers but take longer to reply and use more of your computer's memory.
- `BRAVE_API_KEY` in `.env` is optional and off by default. Without it, the AI only answers from what its model already knows, which for specific game facts (an exact item location, a boss's weak point) is often wrong or a guess, since small local models don't have that level of detail memorized. With it set, the bot runs a quick web search alongside every mention reply and gives the AI the top few results to work from, so those specific answers are much more likely to be accurate - unlike the AI chat itself, this does send your message (with the trigger word stripped out) to Brave's search API over the internet. Get a free key at [brave.com/search/api](https://brave.com/search/api/) (the installer asks for this too and lets you skip it), then restart the bot (re-run the installer, or `pm2 restart discord-bot`) to pick it up.
- `TAVILY_API_KEY` in `.env` is optional and off by default. Without it, the AI only answers from what its model already knows, which for specific game facts (an exact item location, a boss's weak point) is often wrong or a guess, since small local models don't have that level of detail memorized. With it set, the bot runs a quick web search alongside every mention reply and gives the AI the top few results to work from, so those specific answers are much more likely to be accurate - unlike the AI chat itself, this does send your message (with the trigger word stripped out) to Tavily's search API over the internet. Get a free key (no credit card required, 1,000 searches a month free) at [app.tavily.com](https://app.tavily.com/) (the installer asks for this too and lets you skip it), then restart the bot (re-run the installer, or `pm2 restart discord-bot`) to pick it up.
- It remembers each person's last 3 exchanges (their message and its reply) so it can follow up naturally and stick to things you told it earlier in the conversation, like a correction or a preference. This memory is per-person, kept only in the bot's running memory (not saved to disk), and forgotten automatically after 30 minutes of that person not mentioning it.
- The bot's name, trigger word, and personality all come from `personality.yaml`. It ships set up as BMO from Adventure Time:
```
+22 -15
View File
@@ -16,9 +16,9 @@ const STEAM_API_KEY = process.env.STEAM_API_KEY;
const AI_ENABLED = process.env.AI_ENABLED !== "false";
const OLLAMA_URL = process.env.OLLAMA_URL || "http://localhost:11434";
const OLLAMA_MODEL = process.env.OLLAMA_MODEL || "llama3.2";
const BRAVE_API_KEY = process.env.BRAVE_API_KEY;
const WEB_SEARCH_ENABLED = !!BRAVE_API_KEY;
const WEB_SEARCH_RESULT_COUNT = 3;
const TAVILY_API_KEY = process.env.TAVILY_API_KEY;
const WEB_SEARCH_ENABLED = !!TAVILY_API_KEY;
const WEB_SEARCH_RESULT_COUNT = 5;
const WEB_SEARCH_TIMEOUT_MS = 5000;
const AI_COOLDOWN_MS = 5000;
const GREETING_CHANNEL_NAME = "general";
@@ -90,22 +90,25 @@ async function searchWeb(query) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), WEB_SEARCH_TIMEOUT_MS);
try {
const url = "https://api.search.brave.com/res/v1/web/search?q=" + encodeURIComponent(query) +
"&count=" + WEB_SEARCH_RESULT_COUNT;
const res = await fetch(url, {
const res = await fetch("https://api.tavily.com/search", {
method: "POST",
headers: {
"Accept": "application/json",
"Accept-Encoding": "gzip",
"X-Subscription-Token": BRAVE_API_KEY
"Content-Type": "application/json",
"Authorization": "Bearer " + TAVILY_API_KEY
},
body: JSON.stringify({
query: query,
max_results: WEB_SEARCH_RESULT_COUNT,
search_depth: "advanced"
}),
signal: controller.signal
});
if (!res.ok) return [];
const data = await res.json();
const results = (data.web && data.web.results) || [];
const results = data.results || [];
return results.slice(0, WEB_SEARCH_RESULT_COUNT).map((r) => ({
title: stripHtml(r.title),
description: stripHtml(r.description),
description: stripHtml(r.content),
url: r.url || ""
}));
} catch (err) {
@@ -126,7 +129,9 @@ const AI_SYSTEM_PROMPT =
"quip. But for a real question - how to use the bot, or a game question like where to find something or " +
"how to beat something - you can use two or three short sentences to actually give the useful answer; " +
"accuracy and usefulness matter more than brevity there, so don't cut a real answer short just to be " +
"punchy. Never use a numbered or bulleted list. Speak only in " +
"punchy. Never use a numbered or bulleted list. Never include stage directions or action narration of any " +
"kind, with or without asterisks - no \"*whirrs*\", no \"pauses for a moment\", no describing sounds, " +
"gestures, or pauses you're making. Just speak the words out loud, nothing else. Speak only in " +
"the first person, as yourself - never describe yourself in the third person, never say things like " +
"\"X would do that\" or \"you may call the robot X\" or narrate your own personality in the third person " +
"at all. When someone corrects you or asks you to stop doing something, just naturally comply in your " +
@@ -223,9 +228,11 @@ async function askAI(question, history, displayName, allFacts, searchResults) {
if (searchResults && searchResults.length > 0) {
messages.push({
role: "system",
content: "Live search results for this question, in case they help you give an accurate, specific " +
"answer (like an exact item location, boss strategy, or release date). Ignore them completely if " +
"they are not actually relevant, or if this is just casual chat rather than a real question:\n" +
content: "Live search results for this question. If they contain the specific answer (an exact location, " +
"name, or strategy), state it directly and confidently as fact - do not hedge with phrases like " +
"\"I'm not sure\" or \"I think\" when the results actually say it. Only fall back to a general answer " +
"if the results genuinely don't cover it. Ignore them completely if this is just casual chat rather " +
"than a real question:\n" +
searchResults.map((r, i) => (i + 1) + ". " + r.title + " - " + r.description + " (" + r.url + ")").join("\n")
});
}
+6 -6
View File
@@ -123,14 +123,14 @@ if ([string]::IsNullOrWhiteSpace((Get-EnvValue "STEAM_API_KEY"))) {
Set-EnvValue "STEAM_API_KEY" $steamKeyInput
}
if ([string]::IsNullOrWhiteSpace((Get-EnvValue "BRAVE_API_KEY"))) {
if ([string]::IsNullOrWhiteSpace((Get-EnvValue "TAVILY_API_KEY"))) {
Write-Host ""
Write-Host "Optional - lets the AI chat look things up on the web (like exact item locations or boss strategies) instead of only relying on what the AI model already knows:"
Write-Host "1. Go to https://brave.com/search/api/ and sign up for the free plan"
Write-Host "2. Create an API key and copy it"
Write-Host "Optional - lets the AI chat look things up on the web (like exact item locations or boss strategies) instead of only relying on what the AI model already knows. Free, no credit card required:"
Write-Host "1. Go to https://app.tavily.com/ and sign up"
Write-Host "2. Copy the API key it shows you (starts with tvly-)"
Write-Host ""
$braveKeyInput = Read-Host "Paste your Brave Search API key, or press Enter to skip"
Set-EnvValue "BRAVE_API_KEY" $braveKeyInput
$tavilyKeyInput = Read-Host "Paste your Tavily API key, or press Enter to skip"
Set-EnvValue "TAVILY_API_KEY" $tavilyKeyInput
}
if (-not (Test-CommandExists ollama)) {
+7 -7
View File
@@ -131,16 +131,16 @@ if [[ -z "$CURRENT_STEAM_KEY" ]]; then
set_env_value STEAM_API_KEY "$STEAM_KEY_INPUT"
fi
CURRENT_BRAVE_KEY=$(grep -E "^BRAVE_API_KEY=" .env | cut -d "=" -f2-)
CURRENT_TAVILY_KEY=$(grep -E "^TAVILY_API_KEY=" .env | cut -d "=" -f2-)
if [[ -z "$CURRENT_BRAVE_KEY" ]]; then
if [[ -z "$CURRENT_TAVILY_KEY" ]]; then
echo ""
echo "Optional - lets the AI chat look things up on the web (like exact item locations or boss strategies) instead of only relying on what the AI model already knows:"
echo "1. Go to https://brave.com/search/api/ and sign up for the free plan"
echo "2. Create an API key and copy it"
echo "Optional - lets the AI chat look things up on the web (like exact item locations or boss strategies) instead of only relying on what the AI model already knows. Free, no credit card required:"
echo "1. Go to https://app.tavily.com/ and sign up"
echo "2. Copy the API key it shows you (starts with tvly-)"
echo ""
read -p "Paste your Brave Search API key, or press Enter to skip: " BRAVE_KEY_INPUT
set_env_value BRAVE_API_KEY "$BRAVE_KEY_INPUT"
read -p "Paste your Tavily API key, or press Enter to skip: " TAVILY_KEY_INPUT
set_env_value TAVILY_API_KEY "$TAVILY_KEY_INPUT"
fi
if ! command -v ollama >/dev/null 2>&1; then