Add random reply chance to markov bot

Whenever someone says something, there's a chance that markov will
interject his opinion. Users can also set the chance between 0.0 and the
default value (in the config) if they want to see markov replies less
often.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
This commit is contained in:
2022-05-30 14:29:37 -07:00
parent 57dd547233
commit 6a1ed5c372
2 changed files with 52 additions and 29 deletions

View File

@@ -3,6 +3,7 @@ from collections import defaultdict
import dataclasses import dataclasses
import json import json
import logging import logging
import math
from pathlib import Path from pathlib import Path
import random import random
from typing import Any, List, Mapping, Sequence from typing import Any, List, Mapping, Sequence
@@ -35,8 +36,9 @@ def windows(items: Sequence[Any], size: int):
@dataclasses.dataclass @dataclasses.dataclass
class Chain: class Chain:
def __init__(self, order: int, path: Path): def __init__(self, order: int, chance: float, path: Path):
self.order = order self.order = order
self.reply_chance = chance
self.path = path self.path = path
self.__cache = chain_default() self.__cache = chain_default()
self.__last_access = 0.0 self.__last_access = 0.0
@@ -85,19 +87,23 @@ class Chain:
return return
with open(self.path) as fp: with open(self.path) as fp:
log.info("Loading markov chain from %s", self.path) log.info("Loading markov chain from %s", self.path)
self.__cache = defaultdict( obj = json.load(fp)
chain_inner_default,
{ # Load the save object
key: defaultdict( self.reply_chance = obj["reply_chance"]
int, self.__cache = defaultdict(
{ chain_inner_default,
(None if not word else word): weight {
for word, weight in value.items() key: defaultdict(
}, int,
) {
for key, value in json.load(fp).items() (None if not word else word): weight
}, for word, weight in value.items()
) },
)
for key, value in obj["chain"]
},
)
self.__dirty = False self.__dirty = False
def save(self, retain: bool = True): def save(self, retain: bool = True):
@@ -106,18 +112,21 @@ class Chain:
if self.__dirty: if self.__dirty:
log.info("Saving markov chain to %s", self.path) log.info("Saving markov chain to %s", self.path)
self.path.parent.mkdir(parents=True, exist_ok=True) self.path.parent.mkdir(parents=True, exist_ok=True)
# Build the save object
obj = {
"reply_chance": self.reply_chance,
"chain": {
key: {
("" if word is None else word): weight
for word, weight in value.items()
}
for key, value in self.__cache.items()
},
}
with open(self.path, "w") as fp: with open(self.path, "w") as fp:
json.dump( json.dump(obj, fp)
{
key: {
("" if word is None else word): weight
for word, weight in value.items()
}
for key, value in self.__cache.items()
},
fp,
)
self.__dirty = False self.__dirty = False
if not retain: if not retain:
log.debug("Pruning markov chain %s from memory", self.path) log.debug("Pruning markov chain %s from memory", self.path)
self.clear_cache() self.clear_cache()
@@ -162,6 +171,7 @@ class Markov(Plugin):
self.order = int(self.plugin_config.get("order", 1)) self.order = int(self.plugin_config.get("order", 1))
self.data_path = Path(self.plugin_config.get("data_path", "data/markov")) self.data_path = Path(self.plugin_config.get("data_path", "data/markov"))
self.save_every = int(self.plugin_config.get("save_every", 300)) self.save_every = int(self.plugin_config.get("save_every", 300))
self.reply_chance = float(self.plugin_config.get("reply_chance", 0.01))
self.__chains = {} self.__chains = {}
self.__save_loop_task = None self.__save_loop_task = None
self.__saving = asyncio.Lock() self.__saving = asyncio.Lock()
@@ -180,17 +190,22 @@ class Markov(Plugin):
elif line[0] != "!": elif line[0] != "!":
# ignore other commands # ignore other commands
self.add(channel, who.nick, line) self.add(channel, who.nick, line)
# also, maybe generate a sentence
chosen = random.random()
chain = self.get_chain(channel, who)
if chosen <= chain.reply_chance:
pass
def get_chain(self, channel: str, who: str) -> Chain: def get_chain(self, channel: str, who: str) -> Chain:
if channel not in self.__chains: if channel not in self.__chains:
self.__chains[channel] = {} self.__chains[channel] = {}
if who not in self.__chains[channel]: if who not in self.__chains[channel]:
path = self.data_path / channel / who path = self.data_path / channel / who
self.__chains[channel][who] = Chain(self.order, path) self.__chains[channel][who] = Chain(self.order, self.reply_chance, path)
return self.__chains[channel][who] return self.__chains[channel][who]
def add(self, channel: str, who: str, line: str): def add(self, channel: str, who: str, line: str):
if who == self.server_config.nick == who: if who == self.server_config.nick:
return return
chain = self.get_chain(channel, who) chain = self.get_chain(channel, who)
chain.add(line) chain.add(line)
@@ -208,7 +223,7 @@ class Markov(Plugin):
if message: if message:
self.send_to(conn, channel, f"{who.nick}: {message}") self.send_to(conn, channel, f"{who.nick}: {message}")
case ["force", nick] | ["emulate", nick]: case ["force", nick] | ["emulate", nick]:
chain = self.get_chain(channel, who.nick) chain = self.get_chain(channel, nick)
if not chain: if not chain:
return return
message = chain.generate() message = chain.generate()
@@ -219,6 +234,13 @@ class Markov(Plugin):
message = chain.generate() message = chain.generate()
if message: if message:
self.send_to(conn, channel, f"{who.nick}: {message}") self.send_to(conn, channel, f"{who.nick}: {message}")
case ["chance", chance]:
chain = self.get_chain(channel, who.nick)
reply_chance = float(chance)
if not math.isnan(reply_chance):
chain.reply_chance = min(
max(float(reply_chance), 0.0), self.reply_chance
)
case _: case _:
# command not recognized # command not recognized
pass pass

View File

@@ -38,8 +38,9 @@ async def main():
for line in lines: for line in lines:
if mat := LINE_RE.search(line): if mat := LINE_RE.search(line):
name = mat["name"] name = mat["name"]
message = mat["message"] message = mat["message"].strip()
plugin.add(channel, name, message) if name != server_config.nick and message and message[0] != "!":
plugin.add(channel, name, message)
await plugin.save() await plugin.save()