diff --git a/forum/bots/README.md b/forum/bots/README.md new file mode 100644 index 0000000..c1d15bd --- /dev/null +++ b/forum/bots/README.md @@ -0,0 +1,98 @@ +# AI Robot in Forum + +This documentation describes how to use and create AI bots that can reply to posts in the forum. + +--- + +## How to use the AI bot + +There is a pre-built AI bot implemented using OpenAI chat. To enable it, add the following line in `forum/bots_manager.py` and set an environment variable of key and model choice for your AI: + +```python +manager.register_bot(ChatBot(manager=manager, name="bot", id=1)) +``` + +--- + +## How to create your own bot + +1. Create a folder structure like this: + +```bash +forum/bots +├── bot.py +├── my_bot +│ ├── my_bot.py +└── README.md +``` + +2. Implement a class in `my_bot.py`, e.g., `MyBot`, that inherits from `BotBase` (defined in `bot.py`). + + * The `handler` function will be called whenever someone mentions `@bot_name`. + * You can use `self.manager.send_comment(post_id, id, message)` to post a reply. + +3. Register your bot in `forum/bots_manager.py`: + +```python +manager.register_bot(MyBot(manager=manager, name="my_bot", id=2)) +``` + +--- + +## APIs + +### BotsManager + +#### `register_bot(self, bot)` + +Registers a bot with the bot manager. + +**Example:** + +```python +from forum.bots.openai_chat.chat import ChatBot + +manager.register_bot(ChatBot(manager=manager, name="bot", id=1)) +``` + +#### `send_comment(post_id, id, message)` + +Sends a comment to the post with the given `post_id`. + +* `id`: the user ID of the bot posting the comment +* `message`: the comment content + +**Example:** + +```python +self.manager.send_comment(1, 1, "Hello! This is a reply.") # Make sure the id is valid +``` + +--- + +### BotBase + +#### `__init__(self, manager, name, id)` + +Initializes a bot. + +* `manager`: the BotsManager instance +* `name`: the bot's display name +* `id`: the user ID representing the bot + +#### `handler(self, context)` + +This method is triggered when the bot is mentioned (`@bot_name`). +The `context` parameter is a dictionary containing: + +```python +{ + "id": , + "title": , + "author": , + "content": , + "created_at": +} +``` + +Use this function to implement the bot’s behavior and reply using `self.manager.send_comment`. diff --git a/forum/bots/bot.py b/forum/bots/bot.py new file mode 100644 index 0000000..dd32495 --- /dev/null +++ b/forum/bots/bot.py @@ -0,0 +1,8 @@ +class BotBase(): + def __init__(self, manager, name, id): + self.manager = manager + self.name = name + self.id = id + + def handler(self, context): + self.manager.send_comment(context.get(id), self.id, "Hello! I'm bot base.") diff --git a/forum/bots/openai_chat/chat.py b/forum/bots/openai_chat/chat.py new file mode 100644 index 0000000..12de2ee --- /dev/null +++ b/forum/bots/openai_chat/chat.py @@ -0,0 +1,44 @@ +from forum.bots.bot import BotBase + +import os +from openai import OpenAI + +class ChatBot(BotBase): + def __init__(self, manager, name, id): + super().__init__(manager, name, id) + self.client = OpenAI( + # configure it + api_key=os.getenv("DASHSCOPE_API_KEY"), + base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", + ) + + def handler(self, context): + prompt = f""" +Post Information(Please give a single concise reply): +Title: {context.get("title", "")} +Author: {context.get("author", "")} +Created At: {context.get("create_at", "")} + +Content: +{context.get("content", "")} + +Instructions: +- Reply in a friendly, engaging, and lighthearted way. +- Chat about life, fun, or general topics. +- Keep your reply concise. +""".strip() + + try: + completion = self.client.chat.completions.create( + # 模型列表:https://help.aliyun.com/zh/model-studio/getting-started/models + model="qwen-plus", + messages=[ + {"role": "system", "content": "You are the AI (bot) of Lean Forum. Lean only means simple and colorful. Chat about life and fun. Be concise and friendly."}, + {"role": "user", "content": prompt}, + ], + max_tokens="3000" + ) + reply = completion.choices[0].message.content + except Exception as e: + reply = "Something wrong with the bot." + self.manager.send_comment(context.get("id"), self.id, reply) diff --git a/forum/bots/openai_chat/requirements.txt b/forum/bots/openai_chat/requirements.txt new file mode 100644 index 0000000..ec838c5 --- /dev/null +++ b/forum/bots/openai_chat/requirements.txt @@ -0,0 +1 @@ +openai diff --git a/forum/bots_manager.py b/forum/bots_manager.py new file mode 100644 index 0000000..c7fca24 --- /dev/null +++ b/forum/bots_manager.py @@ -0,0 +1,45 @@ +from django.contrib.auth.models import User +from django.db import transaction + +from .models import Comment, Post +from forum.bots.openai_chat.chat import ChatBot +import threading + +class BotsManager(): + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance.bots = {} + return cls._instance + + def at_bot(self, name, post): + bot = self.bots.get(name) + if bot is None: + return + + context = {"id" : post.id, + "title" : post.title, + "author" : post.author, + "content" : post.content, + "created_at" : post.created_at, + } + thread = threading.Thread(target=bot.handler, args=(context,)) + thread.daemon = True + thread.start() + + def register_bot(self, bot): + self.bots[bot.name] = bot + + @staticmethod + def send_comment(post_id, id, message): + with transaction.atomic(): + Comment.objects.create( + post = Post.objects.get(id=post_id), + author = User.objects.get(id=id), + content = message + ) + +manager = BotsManager() +# manager.register_bot(ChatBot(manager=manager, name="bot", id=1)) diff --git a/forum/form.py b/forum/form.py index c248e19..d73e9a0 100644 --- a/forum/form.py +++ b/forum/form.py @@ -1,5 +1,7 @@ from .models import Post, Comment + from django import forms +import re class MDEditorModelForm(forms.ModelForm): def __init__(self, *args, user=None, **kwargs): @@ -14,6 +16,12 @@ class Meta: 'content': '内容', } + def clean_content(self): + content = self.cleaned_data["content"] + mentions = re.findall(r'@(\w+)', content) + self.cleaned_data["mentions"] = mentions + return content + def save(self, commit=True): instance = super().save(commit=False) if self.user: @@ -22,6 +30,7 @@ def save(self, commit=True): instance.save() return instance +# TODO: support @xxx class MDEditorCommentForm(forms.ModelForm): def __init__(self, *args, user=None, post=None, **kwargs): super().__init__(*args, **kwargs) diff --git a/forum/views.py b/forum/views.py index 410dc4b..2ab7f88 100644 --- a/forum/views.py +++ b/forum/views.py @@ -15,6 +15,7 @@ from forum.form import MDEditorCommentForm, MDEditorModelForm from forum.models import Item, Post, Rating +from forum.bots_manager import manager # Create your views here. @@ -22,7 +23,7 @@ "blockquote","b", "i", "strong", "em", "a", "p", "ul", "ol", "li", "code", "pre", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "img", "table", "thead", "tr", "th", "tbody", "td", "sup", "dt", "dd", "dl", - "abbr", "div" + "abbr", "div", "br" ] allowed_attrs = { "a": ["href", "title"], @@ -66,7 +67,12 @@ def post_create(request): if request.method == 'POST': forms = MDEditorModelForm(request.POST, user=request.user) if forms.is_valid(): - forms.save() + post = forms.save() + + mentions = forms.cleaned_data.get("mentions", []) + for mention in mentions: + manager.at_bot(mention, post) + payload = {"head": "Lean Forum", "body": "新帖子发布了,快去看看吧!", "url": "https://lforum.dpdns.org/posts/"} try: send_group_notification(group_name="webpush_new_posts", payload=payload, ttl=1000) diff --git a/requirements.txt b/requirements.txt index 923b351..7e6798f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,5 @@ Markdown>=3.10 packaging>=25.0 sqlparse>=0.5.3 django-webpush>=0.3.6 -bleach>=6.3.0 \ No newline at end of file +bleach>=6.3.0 +openai