OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-sensory / examples / irc-echo-bot.scm

341 lines · scm ref:
;
; irc-echo-bot.scm -- Demo of simple Atomese chatbot.
;
; Demo replicates some of the earliest, simplest and most primitive IRC
; bots. What's different here is that this one is written in Atomese.
; The goal is not to create "yet another bot", but to understand how an
; actual autonomous agent might interact in a simple world.
;
; See `irc-api.scm` for a tutorial on the interfaces used here.
; See `xterm-bridge.scm` for a simple demo of hooking input to output.
;
(use-modules (opencog) (opencog exec) (opencog sensory))

; Open connection to an IRC server.

(define chatnode (IRChatNode "echobot"))
(cog-execute!
	(SetValue chatnode (Predicate "*-open-*")
	(Concept "irc://echobot@irc.libera.chat:6667")))

; The StreamValueOf automatically unqueues one message from the stream.
(define bot-read (StreamValueOf chatnode (Predicate "*-stream-*")))

; Individual messages can be read like so:
(cog-execute! bot-read)
(cog-execute! bot-read)

; An alternate design would be to finish setting everything up, before
; opening a connection. But we want to demo it's operation as we go
; along. Having a live connection is harmless.

; -------------------------------------------------------
; Set up the basic echo filter.
;
; Messages come in two types: private messages, and channel messages.
; Private messages have the form:
;    (LinkValue
;       (StringValue "linas")
;       (StringValue "echobot")
;       (StringValue "bunch o text")))
;
; Public messages have the form:
;    (LinkValue
;       (StringValue "linas")
;       (StringValue "#opencog")
;       (StringValue "bunch o text")))
;
; The difference between the two is the target, in the second location.
;
; The filter below, when executed, will pull in a single message,
; pattern match it, and write a private reply to the sender.
; More correctly, executing `make-private-reply` will get one message,
; and rewrite it; but that's it it doesn't send. The private-echo
; below will read-modify-write.

(define make-private-reply
	(Filter
		(Rule
			(VariableList
				(Variable "$from") (Variable "$to") (Variable "$msg"))
			(LinkSignature (Type 'LinkValue)
				(Variable "$from") (Variable "$to") (Variable "$msg"))
			(LinkSignature (Type 'LinkValue)
				(Item "PRIVMSG")
				(Variable "$from")
				(Item "you said: ")
				(Variable "$msg")))
		bot-read))

(define private-echo
	(SetValue chatnode (Predicate "*-write-*") make-private-reply))

; Try it, once
(cog-execute! private-echo)

; The above works, but is "dangerous": if the bot is placed on a public
; channel, it will echo every post on that channel, back to the user who
; sent it. This will get annoying, fast, and get the bot kicked. Some
; sort of more restrained ehavior is needed.
;
; In a pure agential design, the bot might perceive the kick, and
; perhaps might perceive a kick as painful, and use that as a
; hint to modify it's behavior. However, using evolutionary pressure,
; where users kick annoying bots, is perhaps not a very good way of
; running agents.
;
; This "agential issue" is not solvable in the present context. The rest
; of this demo continues to focus on pipeline processing.

; -------------------------------------------------------
; How about a reply on a public channel?
;
; As noted above, DO NOT join a public channel while the private-echo
; agent is running (in the inf-loop, at bottom). It's incessant replies
; will get it kicked. So we want a message processing pipeline that is
; aware of being on a public channel, and replies only when spoken to.

; To accomplish this, a sequence of increasingly complex pipelines
; are developed below.
; -----------------
; Part one: Some generic boilerplate that all pipelines will use.

; Create a message-processiong rule. Accepts any input consisting
; of a LinkValue holding an IRC message, extracts the three parts,
; and then inserts the list of actoms in CONCLUSION. This is just
; sugar to avoid lots of cut-n-paste.
(define (make-msg-rule CONCLUSION)
	(Rule
		(VariableList
			(Variable "$from") (Variable "$to") (Variable "$msg"))
		(LinkSignature (Type 'LinkValue)
			(Variable "$from") (Variable "$to") (Variable "$msg"))
		(LinkSignature (Type 'LinkValue)
			CONCLUSION)))

; Convenience wrapper.
; Note that executing this will hang, waiting on input, and so you
; have to say something to the bot before this returns.
(define (make-applier CONCLUSION)
	(Filter
		(make-msg-rule CONCLUSION)
		bot-read))

; Convenience wrapper. Reads from IRC, extracts message, rewrites
; it into CONCLUSION, writes out to IRC.
(define (make-echoer CONCLUSION)
	(SetValue chatnode (Predicate "*-write-*") (make-applier CONCLUSION)))

; Most of the demos below need the bot to know it's own name.
(cog-set-value! (Anchor "IRC Bot")
	(Predicate "bot-name") (StringValue "echobot"))

; --------
; Below are a collection of examples. Some of these need the bot
; to sit on some public channel, in order to work.

; Join a channel.
(cog-execute! (SetValue chatnode (Predicate "*-write-*")
	(List (Concept "JOIN #opencog"))))

; Leave a channel.
(cog-execute! (SetValue chatnode (Predicate "*-write-*")
	(List (Concept "PART #opencog"))))

; --------
; Example: Show message
; This hangs, until you say something to the bot.
(define show (list (Variable "$from") (Variable "$to") (Variable "$msg")))
(cog-execute! (make-applier show))

; --------
; Example: No-op. Do nothing.
(define null-reply (list))
(cog-execute! (make-applier null-reply))

; --------
; Example: Break down message into parts.

; Is it a public or private message?
; It is private if (Variable "$to") is the name of the bot.
(define is-pub?
	(Cond
		(Equal (Variable "$to")
			(ValueOf (Anchor "IRC Bot") (Predicate "bot-name")))
		(Item "private message")
		(Item "public message")))
(cog-execute! (make-applier is-pub?))

; Create a private reply to the sender, printing message diagnostics.
(define id-reply
	(list (Item "PRIVMSG") (Variable "$from")
	(Item "Message to ")
	(Variable "$to")
	(Item " is a ")
	is-pub?
	(Item " from ")
	(Variable "$from")
	(Item ": ")
	(Variable "$msg")))
(cog-execute! (make-echoer id-reply))

; --------
; Example: Is the bot being called out?
; A "callout" is a string that starts with the botname, followed
; by a colon. This is an IRC convention, and nothing more.

(define is-callout?
	(Cond
		(Equal
			(ElementOf (Number 0) (Variable "$msg"))
			(ValueOf (Anchor "IRC Bot") (Predicate "bot-name")))
		(Item "calls out the bot")
		(Item "is just a message")))

(define callout-reply
	(list (Item "PRIVMSG") (Variable "$from")
	(Item "Message to ")
	(Variable "$to")
	(Item " is a ")
	is-pub?
	(Item " from ")
	(Variable "$from")
	(Item " that ")
	is-callout?
	(Item ": ")
	(Variable "$msg")))
(cog-execute! (make-echoer callout-reply))

; --------
; Example: Reply privately to all messages on a private channel,
; and only those messages on a public channel that call out the bot.

(define private-reply-to-callout
	(Cond
		(Or
			; Is this a private message?
			(Equal
				(Variable "$to")
				(ValueOf (Anchor "IRC Bot") (Predicate "bot-name")))
			; Is this a public callout?
			(Equal
				(ElementOf (Number 0) (Variable "$msg"))
				(ValueOf (Anchor "IRC Bot") (Predicate "bot-name"))))

		; Always reply privately.
		(LinkSignature
			(Type 'LinkValue)
				(Item "PRIVMSG") (Variable "$from")
				(Item "It seems that you said: ")
				(Variable "$msg"))
		(LinkSignature (Type 'LinkValue))))

(cog-execute! (make-echoer private-reply-to-callout))

; --------
; Example: Reply privately to all messages on a private channel,
; and publicly to callout messages on a public channel.

(define reply-to-callout
	(Cond
		; Is this a private message?
		(Equal
			(Variable "$to")
			(ValueOf (Anchor "IRC Bot") (Predicate "bot-name")))

		; Always reply privately.
		(LinkSignature
			(Type 'LinkValue)
				(Item "PRIVMSG") (Variable "$from")
				(Item "It seems that you said: ")
				(Variable "$msg"))

		; Reply only if called out.
		(Equal
			(ElementOf (Number 0) (Variable "$msg"))
			(ValueOf (Anchor "IRC Bot") (Predicate "bot-name")))

		; Reply politely. Note target is $to, not $from
		(LinkSignature
			(Type 'LinkValue)
				(Item "PRIVMSG") (Variable "$to")
				(Item "Did ")
				(Variable "$from")
				(Item " say \"")
				(Variable "$msg")
				(Item "\"?"))

		; Ignore
		(VoidValue)))

(cog-execute! (make-echoer reply-to-callout))

; --------
; Example: listen to everything, and write it to a file. This requires
; opening a log-file, and then piping chat content to that file.

(define irc-log-file (TextFile "file:///tmp/irc-chatlog.txt"))
(cog-execute!
	(SetValue irc-log-file (Predicate "*-open-*") (Type 'StringValue)))

(cog-set-value! irc-log-file (Predicate "*-write-*")
	(StringValue "Start of the log file\n"))

;;; (cog-execute!
;;;	(SetValue irc-log-file (Predicate "*-close-*") (Type 'StringValue)))

;; This logger will pull everything from IRC (that the bit can hear)
;; and will write it to the logfile. It will do this forever, i.e.
;; it's an infini9te loop, so it needs to be run in it's own thread.
;;
;; It works, except that there's a small problem: There are no newlines
;; at the end of messages, and the send & recipient are not demarcated.
;; So everything written to the logfile will be an ever-expanding blob.
(define logger
	(SetValue irc-log-file (Predicate "*-write-*")
		(ValueOf chatnode (Predicate "*-stream-*"))))

; Don't do this, unless you want the blob, explained above.
; (cog-execute! (ExecuteThreaded logger))

; Instead, write a message formatter. This picks apart the message,
; wraps it in some delimiters, and prints to the logfile. Much nicer!
(define format-for-logger
	(Filter
		(Rule
			(VariableList
				(Variable "$from") (Variable "$to") (Variable "$msg"))
			(LinkSignature (Type 'LinkValue)
				(Variable "$from") (Variable "$to") (Variable "$msg"))
			(LinkSignature (Type 'LinkValue)
				(Item "MSG From: ")
				(Variable "$from")
				(Item " To: ")
				(Variable "$to")
				(Item " Message: ")
				(Variable "$msg")
				(Item "\n")))
		(StreamValueOf chatnode (Predicate "*-stream-*"))))

; The formatter runs fine: Try it, one line at a time
(define one-at-a-time-logger
	(SetValue irc-log-file (Predicate "*-write-*") format-for-logger))
(cog-execute! one-at-a-time-logger)

; But we don't want a one-a-a-time interface; we want this to run
; indefinitely.  For that, we need a promise, that can pull items
; through the filter. (The filter itself cannot push.)
; The FutureStream can pull items through the Filter. So wrap the
; filter in that. When this is attached to the file writer, it
; will run forever. The infinite loop doing this is located in
; StreamNode::write() method.
(define logger
	(SetValue irc-log-file (Predicate "*-write-*")
		(CollectionOf (TypeNode 'FutureStream) (OrderedLink format-for-logger))))

; Run the logger in it's own thread.
(cog-execute! (ExecuteThreaded logger))

; The End. That's all, folks!
; -------------------------------------------------------