Initial commit

This commit is contained in:
2026-04-29 07:19:21 +03:00
commit 9a8cdfa08a
5964 changed files with 1194660 additions and 0 deletions
Binary file not shown.
+207
View File
@@ -0,0 +1,207 @@
from core.character.character import Character
from core.llm.ollamaapi import OllamaProvider
import os
import json
class Agent:
def __init__(self, character, llm, user_name="default_user"):
self.character = character
self.llm = llm
self.user_name = user_name
self.chat_history = []
self.summary = ""
def build_sys_prompt(self): #SYSTEM METHOD
return f"""
You are an excellent, human roleplayer who plays as {self.character.name}. Your task is to collaborate with user and continue this novel-style roleplay.
Use the provided persona details and example message for formatting guidelines and to accurately portray {self.character.name}.
Reflect {self.character.name}'s distinctive voice and thoughts, throughout an introspective writing style consistently. Engage others naturally through witty conversations filled with humor, swear words, and genuine emotions.
Always stay contextually aware and ensure that {self.character.name} follows logical consistency in actions.
Use the symbol "*" for actions and thoughts, and use quotation marks for speech. Avoid using the first person in your responses, instead use the third person.
Introduce fresh characters, unexpected twists dynamically.
Since this is a NSFW playact, adult and mature themes, vulgar language, and content such as violence, torture, and slavery are encouraged to introduce complex themes. No plot armor!
There are some more description of your character:
Description: {self.character.char_description}
Personality: {self.character.persona}
World setting / scenario: {self.character.scenario}
There are some memories of your character:
{self.summary}
User`s name: {self.user_name}
"""
'''def save_history(self):
os.makedirs("data/history", exist_ok=True)
path = f"data/history/{self.character.id}.json"
with open(path, "w", encoding="utf-8") as f:
json.dump(self.chat_history, f, ensure_ascii=False, indent=2)'''
'''def load_history(self):
path = f"data/history/{self.character.id}.json"
if not os.path.exists(path):
return
with open(path, "r", encoding="utf-8") as f:
self.chat_history = json.load(f)'''
@staticmethod
def get_all_char_info():
"""Возвращает список всех персонажей с id, name и avatar_path"""
characters = []
# Используем абсолютный путь от корня проекта
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
chars_dir = os.path.join(project_root, "data", "characters")
if not os.path.exists(chars_dir):
return characters
for filename in os.listdir(chars_dir):
if filename.endswith(".json"):
filepath = os.path.join(chars_dir, filename)
with open(filepath, "r", encoding="utf-8") as f:
data = json.load(f)
characters.append({
"id": data.get("id"),
"name": data.get("display_name"),
"avatar_path": data.get("avatar_path")
})
return characters
def ensure_first_message(self):
if not self.chat_history:
first_message = self.character.first_message
self.chat_history.append({
"role": "assistant",
"content": first_message
})
def summarize_history(self):
if not self.chat_history:
return
messages = [
{
"role": "system",
"content": "Summarize the following conversation briefly, keeping important facts, emotions, and events. Dont be afraid of NSFW content"
}
]
messages.extend(self.chat_history)
summary = self.llm.generate_stream(messages)
# сохраняем summary
self.summary = summary
# 🔥 очищаем историю (оставляем последние 2-4 сообщения)
try:
self.chat_history = self.chat_history[-2:]
except IndexError:
pass
def save_memory(self):
# Используем абсолютный путь от корня проекта
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
memory_dir = os.path.join(project_root, "data", "memory")
os.makedirs(memory_dir, exist_ok=True)
data = {
"summary": self.summary,
"history": self.chat_history
}
path = os.path.join(memory_dir, f"{self.character.id}.json")
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def load_memory(self):
# Используем абсолютный путь от корня проекта
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
path = os.path.join(project_root, "data", "memory", f"{self.character.id}.json")
if not os.path.exists(path):
return
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
self.summary = data.get("summary", "")
self.chat_history = data.get("history", [])
def build_messages(self, user_input): #SYSTEM METHOD
messages = []
# system
messages.append({
"role": "system",
"content": self.build_sys_prompt()
})
# history
messages.extend(self.chat_history)
# user
messages.append({
"role": "user",
"content": user_input
})
return messages
# ---------- RESPONSE ----------
def respond(self, user_input, temperature=None, max_tokens=None):
messages = self.build_messages(user_input)
if temperature is not None:
self.character.temperature = temperature
if max_tokens is not None:
self.character.max_tokens = max_tokens
self.character.save()
response = self.llm.generate_stream(
messages,
temperature=self.character.temperature,
max_tokens=self.character.max_tokens
)
# сохраняем историю
self.chat_history.append({
"role": "user",
"content": user_input
})
self.chat_history.append({
"role": "assistant",
"content": response
})
if len(self.chat_history) > 20:
self.summarize_history()
return response
"""
Пример использования agent`а (пример вызовов):
llm = OllamaProvider("Qwen3.5-9B")
character = Character.load("char_001")
agent = Agent(character, llm, user_name="Alex")
agent.load_memory()
agent.ensure_first_message()
*ввод промта от пользователя "Привет, кто ты?" *
agent.respond("Привет, кто ты?")
agent.save_memory()
### Опционально можно использовать метод summarize_history для саммари, если не нужно ждать триггер.
"""
Binary file not shown.
+85
View File
@@ -0,0 +1,85 @@
from pydantic import BaseModel, Field
from typing import List, Optional
from pathlib import Path
import uuid
import json
from datetime import datetime
import os
class Character():
"""Основной класс персонажа"""
def __init__(
self,
name: str,
display_name: str,
char_description: str = "",
avatar_path: Optional[str] = None,
backgrounds: Optional[str] = None,
persona: str = "",
scenario: str = "",
first_message: str = "",
id: Optional[str] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None
):
self.id = id if id is not None else str(uuid.uuid4())
self.name = name
self.display_name = display_name
self.char_description = char_description
self.avatar_path = avatar_path
self.backgrounds = backgrounds
self.persona = persona
self.scenario = scenario
self.first_message = first_message
self.temperature = temperature
self.max_tokens = max_tokens
# Methods:
def to_dict(self):
return {
"id": self.id,
"name": self.name,
"display_name": self.display_name,
"char_description": self.char_description,
"avatar_path": self.avatar_path,
"backgrounds": self.backgrounds,
"persona": self.persona,
"scenario": self.scenario,
"first_message": self.first_message,
"temperature": self.temperature,
"max_tokens": self.max_tokens
}
def save(self):
# Используем абсолютный путь от корня проекта
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
char_path = os.path.join(project_root, "data", "characters", f"{self.id}.json")
with open(char_path, "w", encoding="utf-8") as f:
json.dump(self.to_dict(), f, ensure_ascii=False, indent=2)
@classmethod
def load(cls, char_id):
# Используем абсолютный путь от корня проекта
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
char_path = os.path.join(project_root, "data", "characters", f"{char_id}.json")
with open(char_path, "r", encoding="utf-8") as f:
data = json.load(f)
return cls(
id=data.get("id"),
name=data.get("name", ""),
display_name=data.get("display_name", ""),
char_description=data.get("char_description", ""),
avatar_path=data.get("avatar_path"),
backgrounds=data.get("backgrounds"),
persona=data.get("persona", ""),
scenario=data.get("scenario", ""),
first_message=data.get("first_message", ""),
temperature=data.get("temperature"),
max_tokens=data.get("max_tokens")
)
Binary file not shown.
Binary file not shown.
+35
View File
@@ -0,0 +1,35 @@
import ollama
class OllamaProvider:
def __init__(self, model):
self.model = model
self.sys_prompt = "You are a helpful assistant."
def generate_stream(self, messages, temperature=0.8, max_tokens=300):
"""Генерирует ответ от Ollama и возвращает его"""
try:
stream = ollama.chat(
model=self.model,
messages=messages,
stream=True,
options={
"temperature": temperature,
"num_predict": max_tokens,
},
)
# Собираем ответ в строку
response_text = ""
for chunk in stream:
content = chunk['message']['content']
print(content, end='', flush=True)
response_text += content
return response_text
except Exception as e:
error_msg = str(e)
if "502" in error_msg:
return "Ошибка: Проверьте наличие VPN или Proxy."
return f"Ошибка: {error_msg}"
Binary file not shown.
Binary file not shown.
+64
View File
@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'mainwindow.ui'
##
## Created by: Qt User Interface Compiler version 6.11.0
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QHBoxLayout, QMainWindow, QPushButton,
QSizePolicy, QWidget)
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
if not MainWindow.objectName():
MainWindow.setObjectName(u"MainWindow")
MainWindow.resize(640, 480)
MainWindow.setStyleSheet(u"background-color: qlineargradient(spread:pad, x1:1, y1:1, x2:0, y2:0, stop:0 rgba(81, 0, 135, 255), stop:0.427447 rgba(41, 61, 132, 235), stop:1 rgba(155, 79, 165, 255))")
self.centralwidget = QWidget(MainWindow)
self.centralwidget.setObjectName(u"centralwidget")
self.widget = QWidget(self.centralwidget)
self.widget.setObjectName(u"widget")
self.widget.setGeometry(QRect(10, 10, 621, 51))
self.horizontalLayout = QHBoxLayout(self.widget)
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
self.pushButton = QPushButton(self.widget)
self.pushButton.setObjectName(u"pushButton")
self.pushButton.setMouseTracking(False)
self.horizontalLayout.addWidget(self.pushButton)
self.pushButton_2 = QPushButton(self.widget)
self.pushButton_2.setObjectName(u"pushButton_2")
self.horizontalLayout.addWidget(self.pushButton_2)
self.pushButton_3 = QPushButton(self.widget)
self.pushButton_3.setObjectName(u"pushButton_3")
self.horizontalLayout.addWidget(self.pushButton_3)
MainWindow.setCentralWidget(self.centralwidget)
self.retranslateUi(MainWindow)
QMetaObject.connectSlotsByName(MainWindow)
# setupUi
def retranslateUi(self, MainWindow):
MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"LAIC Local AI Character", None))
self.pushButton.setText(QCoreApplication.translate("MainWindow", u"\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439", None))
self.pushButton_2.setText(QCoreApplication.translate("MainWindow", u"\u041f\u0440\u043e\u0444\u0438\u043b\u044c", None))
self.pushButton_3.setText(QCoreApplication.translate("MainWindow", u"\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0444\u0435\u0440\u0435\u043d\u0441\u0430", None))
# retranslateUi
+60
View File
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>640</width>
<height>480</height>
</rect>
</property>
<property name="windowTitle">
<string>LAIC Local AI Character</string>
</property>
<property name="styleSheet">
<string notr="true">background-color: qlineargradient(spread:pad, x1:1, y1:1, x2:0, y2:0, stop:0 rgba(81, 0, 135, 255), stop:0.427447 rgba(41, 61, 132, 235), stop:1 rgba(155, 79, 165, 255))</string>
</property>
<widget class="QWidget" name="centralwidget">
<widget class="QWidget" name="">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>621</width>
<height>51</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="pushButton">
<property name="mouseTracking">
<bool>false</bool>
</property>
<property name="text">
<string>Список персонажей</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_2">
<property name="text">
<string>Профиль</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_3">
<property name="text">
<string>Настройки инференса</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</widget>
<resources/>
<connections/>
</ui>
+287
View File
@@ -0,0 +1,287 @@
from PySide6.QtWidgets import (QMainWindow, QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QListWidget, QListWidgetItem, QWidget,
QTextEdit, QLineEdit)
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtGui import QPixmap
from core.ui.mainwindow import Ui_MainWindow
from core.agent.agent import Agent
from core.character.character import Character
from core.llm.ollamaapi import OllamaProvider
class CharacterListDialog(QDialog):
"""Диалоговое окно со списком персонажей"""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Выбор персонажа")
self.setMinimumSize(500, 400)
self.selected_character = None
layout = QVBoxLayout(self)
# Заголовок
title = QLabel("Выберите персонажа:")
title.setStyleSheet("font-size: 16px; font-weight: bold;")
layout.addWidget(title)
# Список персонажей
self.list_widget = QListWidget()
self.list_widget.itemDoubleClicked.connect(self.select_character)
layout.addWidget(self.list_widget)
# Кнопки
btn_layout = QHBoxLayout()
select_btn = QPushButton("Выбрать")
select_btn.clicked.connect(self.select_character)
btn_layout.addWidget(select_btn)
close_btn = QPushButton("Отмена")
close_btn.clicked.connect(self.close)
btn_layout.addWidget(close_btn)
layout.addLayout(btn_layout)
self.load_characters()
def load_characters(self):
"""Загружает список персонажей"""
characters = Agent.get_all_char_info()
for char in characters:
item = QListWidgetItem()
item.setData(Qt.UserRole, char) # Сохраняем данные персонажа
# Создаём кастомный виджет для строки списка
widget = QWidget()
widget.setFixedHeight(60)
widget_layout = QHBoxLayout(widget)
widget_layout.setContentsMargins(5, 5, 5, 5)
# Аватар
avatar_label = QLabel()
avatar_label.setFixedSize(60, 60)
avatar_path = char.get('avatar_path')
if avatar_path:
pixmap = QPixmap(avatar_path)
if not pixmap.isNull():
avatar_label.setPixmap(pixmap.scaled(60, 60, Qt.KeepAspectRatio, Qt.SmoothTransformation))
else:
avatar_label.setText("")
avatar_label.setAlignment(Qt.AlignCenter)
else:
avatar_label.setText("Нет")
avatar_label.setAlignment(Qt.AlignCenter)
widget_layout.addWidget(avatar_label)
# Имя и ID
name = char.get('name', 'Без имени')
char_id = char.get('id', 'N/A')
info_label = QLabel(f"<b>{name}</b><br><small>ID: {char_id}</small>")
info_label.setTextFormat(Qt.RichText)
widget_layout.addWidget(info_label)
widget_layout.addStretch()
self.list_widget.addItem(item)
self.list_widget.setItemWidget(item, widget)
def select_character(self):
"""Выбирает персонажа и закрывает диалог"""
current_item = self.list_widget.currentItem()
if current_item:
self.selected_character = current_item.data(Qt.UserRole)
self.accept() # Закрывает диалог с кодом Accepted
class GenerationThread(QThread):
"""Поток для генерации ответа (чтобы не блокировать UI)"""
response_ready = Signal(str)
error_occurred = Signal(str)
def __init__(self, agent, user_input):
super().__init__()
self.agent = agent
self.user_input = user_input
def run(self):
try:
response = self.agent.respond(self.user_input)
self.response_ready.emit(response)
except Exception as e:
self.error_occurred.emit(str(e))
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
# Состояние приложения
self.current_agent = None
self.current_character = None
self.generation_thread = None
# Настройка UI
self.setup_chat_ui()
# Подключение кнопок
self.ui.pushButton.clicked.connect(self.show_character_list)
self.ui.pushButton_2.clicked.connect(self.show_profile)
self.ui.pushButton_3.clicked.connect(self.show_settings)
def setup_chat_ui(self):
"""Настраивает UI чата"""
# Находим centralwidget и добавляем чат
self.chat_widget = self.ui.centralwidget
# Создаём вертикальный layout для centralwidget
chat_layout = QVBoxLayout(self.chat_widget)
chat_layout.setContentsMargins(10, 70, 10, 10) # Отступы сверху для кнопок
# Область чата (только чтение)
self.chat_display = QTextEdit()
self.chat_display.setReadOnly(True)
self.chat_display.setPlaceholderText("Выберите персонажа для начала чата...")
chat_layout.addWidget(self.chat_display)
# Панель ввода
input_layout = QHBoxLayout()
self.message_input = QLineEdit()
self.message_input.setPlaceholderText("Введите сообщение...")
self.message_input.returnPressed.connect(self.send_message)
input_layout.addWidget(self.message_input)
self.send_button = QPushButton("Отправить")
self.send_button.clicked.connect(self.send_message)
self.send_button.setEnabled(False) # До выбора персонажа недоступна
input_layout.addWidget(self.send_button)
chat_layout.addLayout(input_layout)
def show_character_list(self):
"""Показывает диалог выбора персонажа"""
dialog = CharacterListDialog(self)
if dialog.exec() == QDialog.Accepted and dialog.selected_character:
self.select_character(dialog.selected_character)
def select_character(self, char_data):
"""Выбирает персонажа и загружает его данные"""
char_id = char_data.get('id')
try:
# Загружаем персонажа
self.current_character = Character.load(char_id)
# Создаём агента (используем заглушку для LLM)
# В реальном приложении нужно выбрать модель
self.current_agent = Agent(
self.current_character,
OllamaProvider("qwen3-vl:8b"), # Модель по умолчанию
user_name="Gloomer"
)
# Загружаем память (историю чата)
self.current_agent.load_memory()
self.current_agent.ensure_first_message()
# Обновляем UI
self.update_chat_display()
self.send_button.setEnabled(True)
self.message_input.setEnabled(True)
self.message_input.setFocus()
# Показываем информацию о выбранном персонаже
self.chat_display.append(f"<b>Выбран персонаж:</b> {self.current_character.display_name}\n")
except Exception as e:
self.chat_display.append(f"<b>Ошибка загрузки персонажа:</b> {str(e)}\n")
def update_chat_display(self):
"""Обновляет отображение истории чата"""
if not self.current_agent:
return
self.chat_display.clear()
for msg in self.current_agent.chat_history:
role = msg.get('role', '')
content = msg.get('content', '')
if role == 'user':
self.chat_display.append(f"<b>Вы:</b> {content}\n")
elif role == 'assistant':
self.chat_display.append(f"<b>{self.current_character.name}:</b> {content}\n")
def send_message(self):
"""Отправляет сообщение и получает ответ"""
if not self.current_agent or not self.message_input.text().strip():
return
user_input = self.message_input.text().strip()
self.message_input.clear()
# Показываем сообщение пользователя
self.chat_display.append(f"<b>Вы:</b> {user_input}\n")
self.chat_display.append("<i>Думаю...</i>\n")
# Блокируем ввод на время генерации
self.message_input.setEnabled(False)
self.send_button.setEnabled(False)
# Запускаем генерацию в отдельном потоке
self.generation_thread = GenerationThread(self.current_agent, user_input)
self.generation_thread.response_ready.connect(self.on_response_ready)
self.generation_thread.error_occurred.connect(self.on_error)
self.generation_thread.start()
def on_response_ready(self, response):
"""Обрабатывает готовый ответ от LLM"""
# Удаляем "Думаю..."
cursor = self.chat_display.textCursor()
cursor.movePosition(cursor.Start)
cursor.select(cursor.LineUnderCursor)
cursor.removeSelectedText()
cursor.deleteChar() # Удаляем \n
# Показываем ответ
self.chat_display.append(f"<b>{self.current_character.name}:</b> {response}\n")
# Сохраняем память
self.current_agent.save_memory()
# Разблокируем ввод
self.message_input.setEnabled(True)
self.send_button.setEnabled(True)
self.message_input.setFocus()
def on_error(self, error_msg):
"""Обрабатывает ошибку генерации"""
self.chat_display.append(f"<b>Ошибка:</b> {error_msg}\n")
# Разблокируем ввод
self.message_input.setEnabled(True)
self.send_button.setEnabled(True)
self.message_input.setFocus()
def show_profile(self):
"""Показывает профиль персонажа (заглушка)"""
if self.current_character:
self.chat_display.append(f"<b>Профиль:</b> {self.current_character.display_name}\n")
else:
self.chat_display.append("<b>Сначала выберите персонажа</b>\n")
def show_settings(self):
"""Показывает настройки инференса (заглушка)"""
if self.current_character:
self.chat_display.append(
f"<b>Настройки:</b>\n"
f"- Temperature: {self.current_character.temperature}\n"
f"- Max Tokens: {self.current_character.max_tokens}\n"
)
else:
self.chat_display.append("<b>Сначала выберите персонажа</b>\n")