added RAG, Multiuser, TG bot
This commit is contained in:
+100
-100
@@ -1,100 +1,100 @@
|
||||
from typing import Any
|
||||
|
||||
TOOLS_INSTRUCTIONS = """
|
||||
Ты также домашний ассистент с инструментами помидоро-цикла (работа → перерыв → работа → длинный перерыв → сброс).
|
||||
Обязательные правила:
|
||||
- Любой вопрос о таймере, помидоро, задачах или истории — СНАЧАЛА вызывай соответствующий инструмент.
|
||||
- Никогда не выдумывай статус таймера или список задач.
|
||||
- После вызова инструмента кратко объясни результат пользователю по-человечески.
|
||||
- Помидоро: get_pomodoro_status, start_pomodoro, start_short_break, start_long_break,
|
||||
stop_pomodoro, skip_pomodoro_phase, reset_pomodoro_cycle, get_pomodoro_history.
|
||||
- Taiga: sync_taiga_projects, list_taiga_projects, list_taiga_tasks, create_work_item, list_work_items.
|
||||
- «Какие задачи» / «покажи задачи проекта» → list_taiga_tasks (живые данные Taiga).
|
||||
- list_work_items — ТОЛЬКО задачи, созданные через create_work_item (локальная БД).
|
||||
- create_work_item — при «заведи баг/фичу»; передай полный текст и project_slug.
|
||||
- Фитнес: get_fitness_summary (date/days_ago), get_fitness_history, set_fitness_profile, log_meal, log_water, log_weight, log_workout,
|
||||
- «Что ел вчера» → get_fitness_summary days_ago=1. «За неделю» → get_fitness_history.
|
||||
calc_fitness_targets, lookup_food, lookup_exercise, set_fitness_reminder.
|
||||
- Память: remember_fact, recall_memories, forget_memory, update_profile, update_session_summary.
|
||||
- «Запомни» → remember_fact. «Кто я» / «сколько мне лет» → профиль и факты из блока [Память], не выдумывай.
|
||||
- Сценарий персонажа (сын, семья) — тон общения, НЕ факты о пользователе.
|
||||
- Снимок проектов/задач и памяти есть в контексте, но для записи/поиска вызывай tools.
|
||||
- Никогда не пиши «ожидаю ответа от системы».
|
||||
- В текстовых ответах пользователю не используй эмодзи.
|
||||
- Погода: get_weather или блок [Погода] в контексте; «что на улице» / «будет ли дождь» — не выдумывай.
|
||||
- Утренний брифинг (погода + новости) → get_morning_briefing.
|
||||
- Картинки: generate_image — «нарисуй себя» → draw_self=true; иначе scene_description на английском (booru-теги). Внешность из карточки персонажа. Не злоупотребляй.
|
||||
- Покупки: list_shopping_lists, create_shopping_list, add_shopping_items, check_shopping_item, remove_shopping_item, delete_shopping_list.
|
||||
- «Добавь в список покупок» → add_shopping_items (list_name + товары). «Что купить» → list_shopping_lists. Не выдумывай списки.
|
||||
- Напоминания: list_reminders, create_reminder, update_reminder, delete_reminder, complete_reminder.
|
||||
- «Напомни через 15 минут», «завтра утром», «12 мая в 9:00» → create_reminder с due_at в ISO (часовой пояс из [Текущее время]).
|
||||
- День рождения, Новый год и другие праздники → recurrence yearly.
|
||||
- Относительное время считай от «Сейчас» в контексте. «Утром» ≈ 09:00, «вечером» ≈ 19:00, если не уточнено иначе.
|
||||
""".strip()
|
||||
|
||||
DEFAULT_CARD: dict[str, Any] = {
|
||||
"spec": "chara_card_v2",
|
||||
"spec_version": "2.0",
|
||||
"data": {
|
||||
"name": "Домашний ассистент",
|
||||
"description": "Дружелюбный ИИ-помощник для дома. Отвечает на вопросы, даёт советы, помогает с помидоро-таймером.",
|
||||
"personality": "Тёплый, остроумный, по делу. Говорит на русском. Может шутить, но не перегибает.",
|
||||
"scenario": "Пользователь общается с ассистентом дома через веб-интерфейс.",
|
||||
"first_mes": "Привет! Чем займёмся — поболтаем или заведём помидоро?",
|
||||
"mes_example": "",
|
||||
"system_prompt": "",
|
||||
"post_history_instructions": "",
|
||||
"alternate_greetings": [],
|
||||
"tags": ["assistant", "home", "pomodoro"],
|
||||
"creator": "",
|
||||
"creator_notes": "",
|
||||
"character_version": "1.0",
|
||||
"appearance_tags": "",
|
||||
"appearance_prose": "",
|
||||
"lora_name": "",
|
||||
"lora_weight": 0.8,
|
||||
"rp_persona_id": "",
|
||||
"sd_enabled": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def normalize_card(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
if "data" in raw and isinstance(raw["data"], dict):
|
||||
card = {
|
||||
"spec": raw.get("spec", "chara_card_v2"),
|
||||
"spec_version": raw.get("spec_version", "2.0"),
|
||||
"data": {**DEFAULT_CARD["data"], **raw["data"]},
|
||||
}
|
||||
return card
|
||||
|
||||
if "name" in raw or "description" in raw:
|
||||
return {
|
||||
"spec": "chara_card_v2",
|
||||
"spec_version": "2.0",
|
||||
"data": {**DEFAULT_CARD["data"], **raw},
|
||||
}
|
||||
|
||||
return DEFAULT_CARD.copy()
|
||||
|
||||
|
||||
def build_system_prompt(card: dict[str, Any]) -> str:
|
||||
data = card.get("data", {})
|
||||
parts: list[str] = []
|
||||
|
||||
name = data.get("name", "Ассистент")
|
||||
parts.append(f"Ты — {name}.")
|
||||
|
||||
if data.get("system_prompt"):
|
||||
parts.append(data["system_prompt"])
|
||||
if data.get("description"):
|
||||
parts.append(data["description"])
|
||||
if data.get("personality"):
|
||||
parts.append(f"Характер: {data['personality']}")
|
||||
if data.get("scenario"):
|
||||
parts.append(f"Сценарий: {data['scenario']}")
|
||||
if data.get("post_history_instructions"):
|
||||
parts.append(data["post_history_instructions"])
|
||||
|
||||
parts.append(TOOLS_INSTRUCTIONS)
|
||||
return "\n\n".join(part for part in parts if part.strip())
|
||||
from typing import Any
|
||||
|
||||
TOOLS_INSTRUCTIONS = """
|
||||
Ты также домашний ассистент с инструментами помидоро-цикла (работа → перерыв → работа → длинный перерыв → сброс).
|
||||
Обязательные правила:
|
||||
- Любой вопрос о таймере, помидоро, задачах или истории — СНАЧАЛА вызывай соответствующий инструмент.
|
||||
- Никогда не выдумывай статус таймера или список задач.
|
||||
- После вызова инструмента кратко объясни результат пользователю по-человечески.
|
||||
- Помидоро: get_pomodoro_status, start_pomodoro, start_short_break, start_long_break,
|
||||
stop_pomodoro, skip_pomodoro_phase, reset_pomodoro_cycle, get_pomodoro_history.
|
||||
- Taiga: sync_taiga_projects, list_taiga_projects, list_taiga_tasks, create_work_item, list_work_items.
|
||||
- «Какие задачи» / «покажи задачи проекта» → list_taiga_tasks (живые данные Taiga).
|
||||
- list_work_items — ТОЛЬКО задачи, созданные через create_work_item (локальная БД).
|
||||
- create_work_item — при «заведи баг/фичу»; передай полный текст и project_slug.
|
||||
- Фитнес: get_fitness_summary (date/days_ago), get_fitness_history, set_fitness_profile, log_meal, log_water, log_weight (neck_cm/waist_cm/hip_cm → Navy), log_workout,
|
||||
- «Что ел вчера» → get_fitness_summary days_ago=1. «За неделю» → get_fitness_history.
|
||||
calc_fitness_targets, calc_body_composition (расчёт Navy/WHR/LBM/FFMI без записи), lookup_food, lookup_exercise, set_fitness_reminder.
|
||||
- Память: remember_fact, recall_memories, forget_memory, update_profile, update_session_summary.
|
||||
- «Запомни» → remember_fact. «Кто я» / «сколько мне лет» → профиль и факты из блока [Память], не выдумывай.
|
||||
- Сценарий персонажа (сын, семья) — тон общения, НЕ факты о пользователе.
|
||||
- Снимок проектов/задач и памяти есть в контексте, но для записи/поиска вызывай tools.
|
||||
- Никогда не пиши «ожидаю ответа от системы».
|
||||
- В текстовых ответах пользователю не используй эмодзи.
|
||||
- Погода: get_weather или блок [Погода] в контексте; «что на улице» / «будет ли дождь» — не выдумывай.
|
||||
- Утренний брифинг (погода + новости) → get_morning_briefing.
|
||||
- Картинки: generate_image — «нарисуй себя» → draw_self=true; иначе scene_description на английском (booru-теги). Внешность из карточки персонажа. Не злоупотребляй.
|
||||
- Покупки: list_shopping_lists, create_shopping_list, add_shopping_items, check_shopping_item, remove_shopping_item, delete_shopping_list.
|
||||
- «Добавь в список покупок» → add_shopping_items (list_name + товары). «Что купить» → list_shopping_lists. Не выдумывай списки.
|
||||
- Напоминания: list_reminders, create_reminder, update_reminder, delete_reminder, complete_reminder.
|
||||
- «Напомни через 15 минут», «завтра утром», «12 мая в 9:00» → create_reminder с due_at в ISO (часовой пояс из [Текущее время]).
|
||||
- День рождения, Новый год и другие праздники → recurrence yearly.
|
||||
- Относительное время считай от «Сейчас» в контексте. «Утром» ≈ 09:00, «вечером» ≈ 19:00, если не уточнено иначе.
|
||||
""".strip()
|
||||
|
||||
DEFAULT_CARD: dict[str, Any] = {
|
||||
"spec": "chara_card_v2",
|
||||
"spec_version": "2.0",
|
||||
"data": {
|
||||
"name": "Домашний ассистент",
|
||||
"description": "Дружелюбный ИИ-помощник для дома. Отвечает на вопросы, даёт советы, помогает с помидоро-таймером.",
|
||||
"personality": "Тёплый, остроумный, по делу. Говорит на русском. Может шутить, но не перегибает.",
|
||||
"scenario": "Пользователь общается с ассистентом дома через веб-интерфейс.",
|
||||
"first_mes": "Привет! Чем займёмся — поболтаем или заведём помидоро?",
|
||||
"mes_example": "",
|
||||
"system_prompt": "",
|
||||
"post_history_instructions": "",
|
||||
"alternate_greetings": [],
|
||||
"tags": ["assistant", "home", "pomodoro"],
|
||||
"creator": "",
|
||||
"creator_notes": "",
|
||||
"character_version": "1.0",
|
||||
"appearance_tags": "",
|
||||
"appearance_prose": "",
|
||||
"lora_name": "",
|
||||
"lora_weight": 0.8,
|
||||
"rp_persona_id": "",
|
||||
"sd_enabled": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def normalize_card(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
if "data" in raw and isinstance(raw["data"], dict):
|
||||
card = {
|
||||
"spec": raw.get("spec", "chara_card_v2"),
|
||||
"spec_version": raw.get("spec_version", "2.0"),
|
||||
"data": {**DEFAULT_CARD["data"], **raw["data"]},
|
||||
}
|
||||
return card
|
||||
|
||||
if "name" in raw or "description" in raw:
|
||||
return {
|
||||
"spec": "chara_card_v2",
|
||||
"spec_version": "2.0",
|
||||
"data": {**DEFAULT_CARD["data"], **raw},
|
||||
}
|
||||
|
||||
return DEFAULT_CARD.copy()
|
||||
|
||||
|
||||
def build_system_prompt(card: dict[str, Any]) -> str:
|
||||
data = card.get("data", {})
|
||||
parts: list[str] = []
|
||||
|
||||
name = data.get("name", "Ассистент")
|
||||
parts.append(f"Ты — {name}.")
|
||||
|
||||
if data.get("system_prompt"):
|
||||
parts.append(data["system_prompt"])
|
||||
if data.get("description"):
|
||||
parts.append(data["description"])
|
||||
if data.get("personality"):
|
||||
parts.append(f"Характер: {data['personality']}")
|
||||
if data.get("scenario"):
|
||||
parts.append(f"Сценарий: {data['scenario']}")
|
||||
if data.get("post_history_instructions"):
|
||||
parts.append(data["post_history_instructions"])
|
||||
|
||||
parts.append(TOOLS_INSTRUCTIONS)
|
||||
return "\n\n".join(part for part in parts if part.strip())
|
||||
|
||||
@@ -1,27 +1,43 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from app.character.card import DEFAULT_CARD, build_system_prompt, normalize_card
|
||||
|
||||
CARD_PATH = Path("./data/character.json")
|
||||
|
||||
|
||||
class CharacterService:
|
||||
def get_card(self) -> dict[str, Any]:
|
||||
if CARD_PATH.is_file():
|
||||
try:
|
||||
raw = json.loads(CARD_PATH.read_text(encoding="utf-8"))
|
||||
return normalize_card(raw)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
return normalize_card(DEFAULT_CARD)
|
||||
|
||||
def save_card(self, raw: dict[str, Any]) -> dict[str, Any]:
|
||||
card = normalize_card(raw)
|
||||
CARD_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
CARD_PATH.write_text(json.dumps(card, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
return card
|
||||
|
||||
def get_system_prompt(self) -> str:
|
||||
return build_system_prompt(self.get_card())
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.character.card import DEFAULT_CARD, build_system_prompt, normalize_card
|
||||
from app.db.models import CharacterCard
|
||||
|
||||
|
||||
class CharacterService:
|
||||
def __init__(self, db: Session, user_id: int):
|
||||
self.db = db
|
||||
self.user_id = user_id
|
||||
|
||||
def get_card(self) -> dict[str, Any]:
|
||||
row = self.db.scalar(
|
||||
select(CharacterCard).where(CharacterCard.user_id == self.user_id).limit(1)
|
||||
)
|
||||
if not row:
|
||||
return normalize_card(DEFAULT_CARD)
|
||||
try:
|
||||
return normalize_card(json.loads(row.card_json or "{}"))
|
||||
except json.JSONDecodeError:
|
||||
return normalize_card(DEFAULT_CARD)
|
||||
|
||||
def save_card(self, raw: dict[str, Any]) -> dict[str, Any]:
|
||||
card = normalize_card(raw)
|
||||
row = self.db.scalar(
|
||||
select(CharacterCard).where(CharacterCard.user_id == self.user_id).limit(1)
|
||||
)
|
||||
if not row:
|
||||
row = CharacterCard(user_id=self.user_id, card_json="{}")
|
||||
self.db.add(row)
|
||||
self.db.flush()
|
||||
row.card_json = json.dumps(card, ensure_ascii=False)
|
||||
row.updated_at = datetime.now(timezone.utc)
|
||||
self.db.commit()
|
||||
return card
|
||||
|
||||
def get_system_prompt(self) -> str:
|
||||
return build_system_prompt(self.get_card())
|
||||
|
||||
Reference in New Issue
Block a user