Initial commit
This commit is contained in:
Binary file not shown.
@@ -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.
@@ -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.
@@ -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.
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user