Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions forum/bots/README.md
Original file line number Diff line number Diff line change
@@ -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": <post_id>,
"title": <post_title>,
"author": <author_name>,
"content": <post_content>,
"created_at": <timestamp>
}
```

Use this function to implement the bot’s behavior and reply using `self.manager.send_comment`.
8 changes: 8 additions & 0 deletions forum/bots/bot.py
Original file line number Diff line number Diff line change
@@ -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.")
44 changes: 44 additions & 0 deletions forum/bots/openai_chat/chat.py
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions forum/bots/openai_chat/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
openai
45 changes: 45 additions & 0 deletions forum/bots_manager.py
Original file line number Diff line number Diff line change
@@ -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))
9 changes: 9 additions & 0 deletions forum/form.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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:
Expand All @@ -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)
Expand Down
10 changes: 8 additions & 2 deletions forum/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@

from forum.form import MDEditorCommentForm, MDEditorModelForm
from forum.models import Item, Post, Rating
from forum.bots_manager import manager

# Create your views here.

allowed_tags = [
"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"],
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ Markdown>=3.10
packaging>=25.0
sqlparse>=0.5.3
django-webpush>=0.3.6
bleach>=6.3.0
bleach>=6.3.0
openai