Skip to content

Commit fe22e80

Browse files
authored
Merge pull request #1 from GeneralYadoc/develop
Initial Release
2 parents 907b05f + 7a729d1 commit fe22e80

8 files changed

Lines changed: 392 additions & 1 deletion

File tree

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.gif filter=lfs diff=lfs merge=lfs -text

README.md

Lines changed: 209 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,209 @@
1-
# ChatAIonStream
1+
# ChatAIStream
2+
Message broker between YouTube chat stream and ChatGPT.
3+
4+
## The user of this library can
5+
- pick up massegase from YouTube Chat and generate answer by ChatGPT.
6+
- easily give role to ChatGPT.
7+
8+
## Hou to install
9+
10+
### Install from PyPI
11+
- Install package to your environment.<br>
12+
```install
13+
$ pip install chatai-stream
14+
```
15+
16+
### Install from GitHub repository
17+
- Clone this repository.<br>
18+
```clone
19+
$ clone https://github.com/GeneralYadoc/ChatAIStream.git
20+
```
21+
- Change directory to the root of the repository.<br>
22+
```cd
23+
$ cd ChatAIStream
24+
```
25+
- Install package to your environment.<br>
26+
```install
27+
$ pip install .
28+
```
29+
## How to use
30+
- [OpenAI API Key](https://www.howtogeek.com/885918/how-to-get-an-openai-api-key/) is necessary to execute following sample.
31+
32+
- Sample codes exist [here](samples/sample.py).
33+
``` sample.py
34+
# To execute this sample, please install streamchat-agent from PyPI as follows.
35+
# $ pip install streamchat-agent
36+
import sys
37+
import time
38+
import math
39+
import datetime
40+
import ChatAIStream as cas
41+
42+
# print sentencce by a character incrementally.
43+
def print_incremental(st, interval_sec):
44+
for i in range(len(st)):
45+
if not running:
46+
break
47+
print(f"{st[i]}", end='')
48+
sys.stdout.flush()
49+
interruptible_sleep(interval_sec)
50+
51+
# Customized sleep for making available of running flag interruption.
52+
def interruptible_sleep(time_sec):
53+
counter = math.floor(time_sec / 0.01)
54+
frac = time_sec - (counter * 0.01)
55+
for i in range(counter):
56+
if not running:
57+
break
58+
time.sleep(0.01)
59+
if not running:
60+
return
61+
time.sleep(frac)
62+
63+
# callback for getting answer of ChatGPT
64+
def answer_cb(user_message, completion):
65+
print(f"\n[{user_message.extern.author.name} {user_message.extern.datetime}] {user_message.message}\n")
66+
interruptible_sleep(3)
67+
time_str = datetime.datetime.now().strftime ('%H:%M:%S')
68+
message = completion.choices[0]["message"]["content"]
69+
print(f"[ChatGPT {time_str}] ", end='')
70+
print_incremental(message, 0.05)
71+
print("\n")
72+
interruptible_sleep(5)
73+
74+
running = False
75+
76+
# YouTube Video ID and ChatGPT API Key is given from command line in this example.
77+
if len(sys.argv) <= 2:
78+
exit(0)
79+
80+
# Set params of getting messages from stream source.
81+
stream_params=cas.streamParams(video_id=sys.argv[1])
82+
83+
# Set params of Chat AI.
84+
ai_params=cas.aiParams(
85+
api_key = sys.argv[2],
86+
system_role = "You are a cheerful assistant who speek English and can get conversation exciting with user.",
87+
answer_cb = answer_cb
88+
)
89+
90+
# Create ChatAIStream instance.
91+
ai_stream = cas.ChatAIStream(cas.params(stream_params=stream_params, ai_params=ai_params))
92+
93+
running = True
94+
95+
# Wake up internal thread to get chat messages from stream and ChatGPT answers.
96+
ai_stream.start()
97+
98+
# Wait any key inputted from keyboad.
99+
input()
100+
101+
# Turn off runnging flag in order to finish printing fung of dhit sample.
102+
running=False
103+
104+
# Finish getting ChatGPT answers.
105+
# Internal thread will stop soon.
106+
ai_stream.disconnect()
107+
108+
# terminating internal thread.
109+
ai_stream.join()
110+
111+
del ai_stream
112+
113+
```
114+
115+
- Usage of the sample
116+
```usage
117+
$ python3 ./sample2.py VIDEO-ID OpenAI-API-KEY
118+
```
119+
- Output of the sample<br>
120+
The outputs of the right window are provided by this sample.<br>
121+
Left outputs are also available by ChatAIStream.
122+
![](ReadMeParts/ChatAIAgent.gif)
123+
124+
## Arguments of Constructor
125+
- ChatAIStream object can be configured with following params given to constructor.
126+
127+
### steamParams
128+
| name | description | default |
129+
|------|------------|---------|
130+
| video_id | String following after 'v=' in url of target YouTube live | - |
131+
| get_item_cb | Chat items are thrown to this callback | None |
132+
| pre_filter_cb | Filter set before internal queue | None |
133+
| post_filter_cb | Filter set between internal queue and get_item_cb | None |
134+
| max_queue_size | Max slots of internal queue (0 is no limit) | 1000 |
135+
| interval_sec | Polling interval of picking up items from YouTube | 0.01 \[sec\] |
136+
### aiParams
137+
138+
139+
| name | description | default |
140+
|------|------------|---------|
141+
| api_key | API Key string of OpenAI | - |
142+
| system_role | API Key string of OpenAI | "You are a helpful assistant." |
143+
| ask_cb | user message given to ChatGPT is thrown to this callback | None |
144+
| max_messages_in_context | Max messages in context given to ChatGPT | 20 |
145+
| answer_cb | ChatGPT answer is thrown to this callback | None |
146+
| max_queue_size | Max slots of internal queue (0 is no limit) | 10 |
147+
| model | Model of AI to be used. | None |
148+
| max_tokens_per_request | Max number of tokens which can be contained in a request. | 256 |
149+
| interval_sec | Interval of ChatGPT API call | 20.0 \[sec\] |
150+
### Notice
151+
- Please refer [pytchat README](https://github.com/taizan-hokuto/pytchat) to know the type of YouTube Chat item used by get_item_cb, pre_filter_cb and post filter_cb.
152+
- Stamps in a message and messages consisted by stamps only are removed defaultly even if user doesn't set pre_filter_cb.
153+
- Default value of interval_sec is 20.0, since free user of OpenAI API can get only 3 completions per minitue.
154+
- The system role given by user remains ever as the oldest sentence of current context even if the number of messages is reached to the maximum, so ChatGPT doesn't forgot the role while current cunversation.
155+
156+
## Methods
157+
### start()
158+
- Start YouTube Chat polling and ChatGPT conversation, then start calling user callbacks asyncronously.
159+
- No arguments required, nothing returns.
160+
161+
### join()
162+
- Wait terminating internal threads kicked by start().
163+
- No arguments required, nothing returns.
164+
165+
### connect()
166+
- Start YouTube Chat polling and ChatGPT conversation, then start calling user callbacks syncronously.
167+
- Lines following the call of the method never executen before terminate of internal threads.
168+
- No arguments required, nothing returns.
169+
170+
### disconnect()
171+
- Request to terminate YouTube Chat polling, ChatGPT conversation and calling user callbacks.
172+
- Internal process will be terminated soon after.
173+
- No arguments required, nothing returns.
174+
175+
And other [threading.Thread](https://docs.python.org/3/library/threading.html) public pethods are available.
176+
177+
## Callbacks
178+
### get_item_cb
179+
- Callback for getting YouTube chat items.
180+
- You can implement several processes in it.
181+
- YouTube chat item is thrown as an argument.
182+
- It's not be assumed that any values are returned.
183+
### pre_filter_cb
184+
- pre putting queue filter.
185+
- YouTube chat item is thrown as an argument.
186+
- You can edit YouTube chat items before putting internal queue.
187+
- It's required that edited chat item is returned.
188+
- You can avoid putting internal queue by returning None.
189+
### post_filter_cb
190+
- post getting queue filter
191+
- You can edit YouTube chat items after popping internal queue.
192+
- It's required that edited chat item is returned.
193+
- You can avoid sending item to get_item_cb by returning None.
194+
### ask_cb
195+
- Callback for getting questions that actually thrown to ChatGPT.
196+
- If you register external info to user message when you put it, you can obtain the external info here.
197+
- It's not be assumed that any values are returned.
198+
### answer_cb
199+
- Callback for getting question and answer of ChatGPT
200+
- The type of completion is mentioned [here](https://platform.openai.com/docs/guides/chat).
201+
- It's not be assumed that any values are returned.
202+
203+
## Links
204+
StreamingChaatAgent uses following libraries internally.
205+
206+
- [streamchat-agent](https://github.com/GeneralYadoc/StreamChatAgent)<br>
207+
YouTube chat poller which can get massages very smothly by using internal queue.
208+
- [chatai-agent](https://github.com/GeneralYadoc/ChatAIAgent)<br>
209+
Message broker between user and ChatGPT.

ReadMeParts/ChatAIAgent.gif

Lines changed: 3 additions & 0 deletions
Loading

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
streamchat-agent==1.0.1
2+
chatai-agent==1.0.0

samples/sample.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# To execute this sample, please install streamchat-agent from PyPI as follows.
2+
# $ pip install streamchat-agent
3+
import sys
4+
import time
5+
import math
6+
import datetime
7+
import ChatAIStream as cas
8+
9+
# print sentencce by a character incrementally.
10+
def print_incremental(st, interval_sec):
11+
for i in range(len(st)):
12+
if not running:
13+
break
14+
print(f"{st[i]}", end='')
15+
sys.stdout.flush()
16+
interruptible_sleep(interval_sec)
17+
18+
# Customized sleep for making available of running flag interruption.
19+
def interruptible_sleep(time_sec):
20+
counter = math.floor(time_sec / 0.01)
21+
frac = time_sec - (counter * 0.01)
22+
for i in range(counter):
23+
if not running:
24+
break
25+
time.sleep(0.01)
26+
if not running:
27+
return
28+
time.sleep(frac)
29+
30+
# callback for getting answer of ChatGPT
31+
def answer_cb(user_message, completion):
32+
print(f"\n[{user_message.extern.author.name} {user_message.extern.datetime}] {user_message.message}\n")
33+
interruptible_sleep(3)
34+
time_str = datetime.datetime.now().strftime ('%H:%M:%S')
35+
message = completion.choices[0]["message"]["content"]
36+
print(f"[ChatGPT {time_str}] ", end='')
37+
print_incremental(message, 0.05)
38+
print("\n")
39+
interruptible_sleep(5)
40+
41+
running = False
42+
43+
# YouTube Video ID and ChatGPT API Key is given from command line in this example.
44+
if len(sys.argv) <= 2:
45+
exit(0)
46+
47+
# Set params of getting messages from stream source.
48+
stream_params=cas.streamParams(video_id=sys.argv[1])
49+
50+
# Set params of Chat AI.
51+
ai_params=cas.aiParams(
52+
api_key = sys.argv[2],
53+
system_role = "You are a cheerful assistant who speek English and can get conversation exciting with user.",
54+
answer_cb = answer_cb
55+
)
56+
57+
# Create ChatAIStream instance.
58+
ai_stream = cas.ChatAIStream(cas.params(stream_params=stream_params, ai_params=ai_params))
59+
60+
running = True
61+
62+
# Wake up internal thread to get chat messages from stream and ChatGPT answers.
63+
ai_stream.start()
64+
65+
# Wait any key inputted from keyboad.
66+
input()
67+
68+
# Turn off runnging flag in order to finish printing fung of dhit sample.
69+
running=False
70+
71+
# Finish getting ChatGPT answers.
72+
# Internal thread will stop soon.
73+
ai_stream.disconnect()
74+
75+
# terminating internal thread.
76+
ai_stream.join()
77+
78+
del ai_stream

setup.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from glob import glob
2+
from os.path import basename
3+
from os.path import splitext
4+
5+
from setuptools import setup
6+
from setuptools import find_packages
7+
8+
9+
def _requires_from_file(filename):
10+
return open(filename).read().splitlines()
11+
12+
13+
setup(
14+
name="chatai-stream",
15+
version="0.0.1",
16+
license="MIT",
17+
description="ChatGPT reacts YouTube chat messages.",
18+
author="General Yadoc",
19+
author_email="133023047+GeneralYadoc@users.noreply.github.com",
20+
classifiers=[
21+
'Development Status :: 4 - Beta',
22+
'Programming Language :: Python',
23+
'Programming Language :: Python :: 3.7',
24+
'Programming Language :: Python :: 3.8',
25+
'Programming Language :: Python :: 3.9',
26+
'License :: OSI Approved :: MIT License',
27+
],
28+
url="https://github.com/GeneralYadoc/ChatAIStream",
29+
packages=find_packages("src"),
30+
package_dir={"": "src"},
31+
py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')],
32+
include_package_data=True,
33+
zip_safe=False,
34+
install_requires=_requires_from_file('requirements.txt'),
35+
setup_requires=["pytest-runner"],
36+
tests_require=["pytest", "pytest-cov"]
37+
)

src/ChatAIStream.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import re
2+
import threading
3+
from typing import Callable
4+
from dataclasses import dataclass
5+
import StreamChatAgent as sca
6+
import ChatAIAgent as ca
7+
8+
9+
streamParams = sca.params
10+
aiParams = ca.params
11+
12+
@dataclass
13+
class params():
14+
stream_params: streamParams
15+
ai_params: aiParams
16+
17+
class ChatAIStream(threading.Thread):
18+
def my_pre_filter_cb(self, c):
19+
prefiltered_c = c
20+
prefiltered_c.message = re.sub(r':[^:]+:', ".", prefiltered_c.message)
21+
prefiltered_c.message = re.sub(r'^[\.]+', "", prefiltered_c.message)
22+
if prefiltered_c and self.pre_filter_cb:
23+
prefiltered_c = self.pre_filter_cb(prefiltered_c)
24+
return None if prefiltered_c.message == "" else prefiltered_c
25+
26+
def ask_stream_message_to_ai(self, c):
27+
if self.get_stream_message_cb:
28+
self.get_stream_message_cb(c)
29+
if self.ai_agent:
30+
self.ai_agent.put_message(ca.userMessage(message=c.message, extern=c))
31+
32+
def default_answer_cb(self, user_message, completion):
33+
print("testtest")
34+
answer = completion.choices[0]["message"]["content"]
35+
print(f"[Answer] {answer}")
36+
37+
def __init__( self, params):
38+
self.get_stream_message_cb=params.stream_params.get_item_cb
39+
params.stream_params.get_item_cb=self.ask_stream_message_to_ai
40+
self.pre_filter_cb=params.stream_params.pre_filter_cb
41+
params.stream_params.pre_filter_cb=self.my_pre_filter_cb
42+
43+
self.ai_agent = ca.ChatAIAgent( params.ai_params )
44+
self.stream_agent = sca.StreamChatAgent( params.stream_params )
45+
46+
super(ChatAIStream, self).__init__(daemon=True)
47+
48+
def run(self):
49+
self.stream_agent.start()
50+
self.ai_agent.start()
51+
52+
def connect(self):
53+
self.start()
54+
self.join()
55+
56+
def disconnect(self):
57+
self.ai_agent.disconnect()
58+
self.stream_agent.disconnect()
59+

0 commit comments

Comments
 (0)