479 lines
16 KiB
Python
479 lines
16 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""
|
|||
|
|
Внешний Гиппокамп Montana
|
|||
|
|
=========================
|
|||
|
|
|
|||
|
|
Цифровая эмуляция биологического механизма памяти.
|
|||
|
|
Переживает смерть носителя.
|
|||
|
|
|
|||
|
|
Компоненты:
|
|||
|
|
- Детектор новизны (is_raw_thought)
|
|||
|
|
- Pattern separation (save_to_stream)
|
|||
|
|
- Просмотр потока (view_stream)
|
|||
|
|
- Статистика памяти (memory_stats)
|
|||
|
|
|
|||
|
|
Использование:
|
|||
|
|
python hippocampus.py --view # Просмотр последних мыслей
|
|||
|
|
python hippocampus.py --stats # Статистика памяти
|
|||
|
|
python hippocampus.py --test # Запуск тестов
|
|||
|
|
python hippocampus.py --disney # Анализ по стратегии Диснея
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
from dataclasses import dataclass
|
|||
|
|
from datetime import datetime, timedelta
|
|||
|
|
from pathlib import Path
|
|||
|
|
from typing import Optional
|
|||
|
|
import argparse
|
|||
|
|
|
|||
|
|
|
|||
|
|
@dataclass
|
|||
|
|
class Thought:
|
|||
|
|
"""Единица памяти — координата в 4D пространстве"""
|
|||
|
|
user_id: int
|
|||
|
|
username: str
|
|||
|
|
timestamp: str
|
|||
|
|
thought: str
|
|||
|
|
lang: str
|
|||
|
|
|
|||
|
|
# Дополнительные якоря (опционально)
|
|||
|
|
location: Optional[str] = None # GPS координаты
|
|||
|
|
music_track: Optional[str] = None # Музыка момента
|
|||
|
|
image_url: Optional[str] = None # Визуальный якорь
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ExternalHippocampus:
|
|||
|
|
"""
|
|||
|
|
Внешний Гиппокамп Montana
|
|||
|
|
|
|||
|
|
Эмулирует биологический механизм памяти:
|
|||
|
|
- Pattern separation: каждая мысль = отдельная координата
|
|||
|
|
- Детектор новизны: фильтрует мысли от вопросов/команд
|
|||
|
|
- Консолидация: синхронизация каждые 12 сек
|
|||
|
|
|
|||
|
|
Критическое отличие: переживает смерть носителя
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
def __init__(self, data_dir: Optional[str] = None):
|
|||
|
|
self.data_dir = Path(data_dir) if data_dir else Path(__file__).parent / "data"
|
|||
|
|
self.data_dir.mkdir(exist_ok=True)
|
|||
|
|
self.stream_file = self.data_dir / "stream.jsonl"
|
|||
|
|
|
|||
|
|
# Триггеры для базы знаний
|
|||
|
|
self.knowledge_triggers = ['гиппокамп', 'память', 'поток', 'паттерн', 'днк']
|
|||
|
|
|
|||
|
|
# === ДЕТЕКТОР НОВИЗНЫ ===
|
|||
|
|
|
|||
|
|
def is_raw_thought(self, text: str) -> bool:
|
|||
|
|
"""
|
|||
|
|
Детектор новизны — определяет, является ли текст сырой мыслью
|
|||
|
|
|
|||
|
|
Эмулирует работу биологического гиппокампа:
|
|||
|
|
- Сравнивает с известными паттернами (вопросы, команды)
|
|||
|
|
- Возвращает True если это НОВАЯ мысль
|
|||
|
|
|
|||
|
|
Критерии:
|
|||
|
|
- Длина < 500 символов
|
|||
|
|
- Не вопрос (без ?)
|
|||
|
|
- Не команда (покажи/расскажи/помоги)
|
|||
|
|
"""
|
|||
|
|
text = text.strip()
|
|||
|
|
|
|||
|
|
# Слишком длинное — не мысль
|
|||
|
|
if len(text) > 500:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# Вопрос — не мысль
|
|||
|
|
if text.endswith("?"):
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# Команды — не мысли
|
|||
|
|
command_patterns = [
|
|||
|
|
"покажи", "расскажи", "помоги", "объясни",
|
|||
|
|
"найди", "открой", "запусти", "сделай",
|
|||
|
|
"/start", "/help", "/level", "/cognitive"
|
|||
|
|
]
|
|||
|
|
text_lower = text.lower()
|
|||
|
|
for pattern in command_patterns:
|
|||
|
|
if text_lower.startswith(pattern):
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# Слишком короткое — скорее всего не мысль
|
|||
|
|
if len(text) < 5:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# Это сырая мысль
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
# === PATTERN SEPARATION ===
|
|||
|
|
|
|||
|
|
def save_to_stream(
|
|||
|
|
self,
|
|||
|
|
user_id: int,
|
|||
|
|
username: str,
|
|||
|
|
thought: str,
|
|||
|
|
lang: str = "ru",
|
|||
|
|
location: Optional[str] = None,
|
|||
|
|
music_track: Optional[str] = None
|
|||
|
|
) -> Thought:
|
|||
|
|
"""
|
|||
|
|
Pattern Separation — сохраняет мысль как уникальную координату
|
|||
|
|
|
|||
|
|
Эмулирует биологический паттерн:
|
|||
|
|
- Каждая новая мысль кодируется отдельно
|
|||
|
|
- Append-only (необратимость времени)
|
|||
|
|
- Временная метка UTC
|
|||
|
|
"""
|
|||
|
|
entry = Thought(
|
|||
|
|
user_id=user_id,
|
|||
|
|
username=username,
|
|||
|
|
timestamp=datetime.utcnow().isoformat() + "Z",
|
|||
|
|
thought=thought,
|
|||
|
|
lang=lang,
|
|||
|
|
location=location,
|
|||
|
|
music_track=music_track
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Append-only запись
|
|||
|
|
with open(self.stream_file, "a", encoding="utf-8") as f:
|
|||
|
|
data = {
|
|||
|
|
"user_id": entry.user_id,
|
|||
|
|
"username": entry.username,
|
|||
|
|
"timestamp": entry.timestamp,
|
|||
|
|
"thought": entry.thought,
|
|||
|
|
"lang": entry.lang
|
|||
|
|
}
|
|||
|
|
if entry.location:
|
|||
|
|
data["location"] = entry.location
|
|||
|
|
if entry.music_track:
|
|||
|
|
data["music_track"] = entry.music_track
|
|||
|
|
|
|||
|
|
f.write(json.dumps(data, ensure_ascii=False) + "\n")
|
|||
|
|
|
|||
|
|
return entry
|
|||
|
|
|
|||
|
|
# === ПРОСМОТР ПОТОКА ===
|
|||
|
|
|
|||
|
|
def view_stream(self, limit: int = 10, user_id: Optional[int] = None) -> list[Thought]:
|
|||
|
|
"""
|
|||
|
|
Просмотр потока мыслей
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
limit: количество последних мыслей
|
|||
|
|
user_id: фильтр по пользователю (опционально)
|
|||
|
|
"""
|
|||
|
|
if not self.stream_file.exists():
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
thoughts = []
|
|||
|
|
with open(self.stream_file, "r", encoding="utf-8") as f:
|
|||
|
|
for line in f:
|
|||
|
|
if line.strip():
|
|||
|
|
data = json.loads(line)
|
|||
|
|
if user_id is None or data.get("user_id") == user_id:
|
|||
|
|
thoughts.append(Thought(
|
|||
|
|
user_id=data.get("user_id", 0),
|
|||
|
|
username=data.get("username", "unknown"),
|
|||
|
|
timestamp=data.get("timestamp", ""),
|
|||
|
|
thought=data.get("thought", ""),
|
|||
|
|
lang=data.get("lang", "ru"),
|
|||
|
|
location=data.get("location"),
|
|||
|
|
music_track=data.get("music_track")
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
# Последние N мыслей
|
|||
|
|
return thoughts[-limit:]
|
|||
|
|
|
|||
|
|
# === СТАТИСТИКА ===
|
|||
|
|
|
|||
|
|
def memory_stats(self, user_id: Optional[int] = None) -> dict:
|
|||
|
|
"""
|
|||
|
|
Статистика памяти
|
|||
|
|
|
|||
|
|
Возвращает:
|
|||
|
|
- total_thoughts: общее количество мыслей
|
|||
|
|
- unique_users: уникальные пользователи
|
|||
|
|
- languages: распределение по языкам
|
|||
|
|
- density: плотность кодирования (мыслей в день)
|
|||
|
|
"""
|
|||
|
|
thoughts = self.view_stream(limit=10000, user_id=user_id)
|
|||
|
|
|
|||
|
|
if not thoughts:
|
|||
|
|
return {
|
|||
|
|
"total_thoughts": 0,
|
|||
|
|
"unique_users": 0,
|
|||
|
|
"languages": {},
|
|||
|
|
"density": 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Базовая статистика
|
|||
|
|
total = len(thoughts)
|
|||
|
|
users = set(t.user_id for t in thoughts)
|
|||
|
|
langs = {}
|
|||
|
|
for t in thoughts:
|
|||
|
|
langs[t.lang] = langs.get(t.lang, 0) + 1
|
|||
|
|
|
|||
|
|
# Плотность кодирования
|
|||
|
|
if total >= 2:
|
|||
|
|
first = datetime.fromisoformat(thoughts[0].timestamp.replace("Z", ""))
|
|||
|
|
last = datetime.fromisoformat(thoughts[-1].timestamp.replace("Z", ""))
|
|||
|
|
days = max(1, (last - first).days)
|
|||
|
|
density = round(total / days, 2)
|
|||
|
|
else:
|
|||
|
|
density = total
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"total_thoughts": total,
|
|||
|
|
"unique_users": len(users),
|
|||
|
|
"languages": langs,
|
|||
|
|
"density": density,
|
|||
|
|
"first_thought": thoughts[0].timestamp if thoughts else None,
|
|||
|
|
"last_thought": thoughts[-1].timestamp if thoughts else None
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# === ПОИСК ===
|
|||
|
|
|
|||
|
|
def search(self, query: str, limit: int = 10) -> list[Thought]:
|
|||
|
|
"""
|
|||
|
|
Простой поиск по мыслям
|
|||
|
|
|
|||
|
|
Для семантического поиска используйте RAG систему.
|
|||
|
|
"""
|
|||
|
|
thoughts = self.view_stream(limit=10000)
|
|||
|
|
query_lower = query.lower()
|
|||
|
|
|
|||
|
|
results = []
|
|||
|
|
for thought in thoughts:
|
|||
|
|
if query_lower in thought.thought.lower():
|
|||
|
|
results.append(thought)
|
|||
|
|
|
|||
|
|
return results[:limit]
|
|||
|
|
|
|||
|
|
# === ТЕСТЫ ===
|
|||
|
|
|
|||
|
|
def run_tests(self) -> dict:
|
|||
|
|
"""
|
|||
|
|
Запуск тестов детектора новизны
|
|||
|
|
"""
|
|||
|
|
test_cases = [
|
|||
|
|
# (текст, ожидаемый результат, описание)
|
|||
|
|
("Время не движется, я движусь", True, "Сырая мысль"),
|
|||
|
|
("Маска тяжелее лица", True, "Сырая мысль"),
|
|||
|
|
("Я создаю свою игру", True, "Сырая мысль"),
|
|||
|
|
("Что такое ACP?", False, "Вопрос"),
|
|||
|
|
("Как работает Montana?", False, "Вопрос"),
|
|||
|
|
("Покажи документацию", False, "Команда"),
|
|||
|
|
("Расскажи про гиппокамп", False, "Команда"),
|
|||
|
|
("/start", False, "Telegram команда"),
|
|||
|
|
("/help", False, "Telegram команда"),
|
|||
|
|
("Ок", False, "Слишком короткое"),
|
|||
|
|
("Да", False, "Слишком короткое"),
|
|||
|
|
("A" * 600, False, "Слишком длинное"),
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
passed = 0
|
|||
|
|
failed = 0
|
|||
|
|
results = []
|
|||
|
|
|
|||
|
|
for text, expected, description in test_cases:
|
|||
|
|
actual = self.is_raw_thought(text)
|
|||
|
|
status = "✓" if actual == expected else "✗"
|
|||
|
|
|
|||
|
|
if actual == expected:
|
|||
|
|
passed += 1
|
|||
|
|
else:
|
|||
|
|
failed += 1
|
|||
|
|
|
|||
|
|
results.append({
|
|||
|
|
"text": text[:50] + "..." if len(text) > 50 else text,
|
|||
|
|
"expected": expected,
|
|||
|
|
"actual": actual,
|
|||
|
|
"status": status,
|
|||
|
|
"description": description
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"total": len(test_cases),
|
|||
|
|
"passed": passed,
|
|||
|
|
"failed": failed,
|
|||
|
|
"results": results
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# === ЭКСПОРТ ===
|
|||
|
|
|
|||
|
|
def export_markdown(self, user_id: Optional[int] = None) -> str:
|
|||
|
|
"""
|
|||
|
|
Экспорт памяти в Markdown
|
|||
|
|
|
|||
|
|
Для потомков.
|
|||
|
|
"""
|
|||
|
|
thoughts = self.view_stream(limit=10000, user_id=user_id)
|
|||
|
|
stats = self.memory_stats(user_id)
|
|||
|
|
|
|||
|
|
lines = [
|
|||
|
|
f"# Память Montana",
|
|||
|
|
f"",
|
|||
|
|
f"**Всего мыслей:** {stats['total_thoughts']}",
|
|||
|
|
f"**Плотность кодирования:** {stats['density']} мыслей/день",
|
|||
|
|
f"",
|
|||
|
|
f"---",
|
|||
|
|
f"",
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
current_date = None
|
|||
|
|
for thought in thoughts:
|
|||
|
|
# Группировка по дням
|
|||
|
|
date = thought.timestamp[:10]
|
|||
|
|
if date != current_date:
|
|||
|
|
current_date = date
|
|||
|
|
lines.append(f"## {date}")
|
|||
|
|
lines.append("")
|
|||
|
|
|
|||
|
|
time = thought.timestamp[11:16]
|
|||
|
|
lines.append(f"**[{time}]** {thought.thought}")
|
|||
|
|
lines.append("")
|
|||
|
|
|
|||
|
|
return "\n".join(lines)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def print_thoughts(thoughts: list[Thought]):
|
|||
|
|
"""Красивый вывод мыслей"""
|
|||
|
|
for thought in thoughts:
|
|||
|
|
time = thought.timestamp[:16].replace("T", " ")
|
|||
|
|
print(f"[{time}] @{thought.username} ({thought.lang})")
|
|||
|
|
print(f" {thought.thought}")
|
|||
|
|
print()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def print_stats(stats: dict):
|
|||
|
|
"""Красивый вывод статистики"""
|
|||
|
|
print(f"Ɉ Статистика памяти Montana")
|
|||
|
|
print()
|
|||
|
|
print(f" Всего мыслей: {stats['total_thoughts']}")
|
|||
|
|
print(f" Пользователей: {stats['unique_users']}")
|
|||
|
|
print(f" Плотность: {stats['density']} мыслей/день")
|
|||
|
|
print()
|
|||
|
|
print(f" Языки:")
|
|||
|
|
for lang, count in stats.get('languages', {}).items():
|
|||
|
|
print(f" {lang}: {count}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def print_tests(results: dict):
|
|||
|
|
"""Красивый вывод результатов тестов"""
|
|||
|
|
print(f"Ɉ Тест детектора новизны")
|
|||
|
|
print()
|
|||
|
|
|
|||
|
|
for r in results['results']:
|
|||
|
|
expected = "МЫСЛЬ" if r['expected'] else "НЕ МЫСЛЬ"
|
|||
|
|
actual = "МЫСЛЬ" if r['actual'] else "НЕ МЫСЛЬ"
|
|||
|
|
status = r['status']
|
|||
|
|
print(f"{status} [{r['description']}]")
|
|||
|
|
print(f" Текст: \"{r['text']}\"")
|
|||
|
|
print(f" Ожидалось: {expected}, Получено: {actual}")
|
|||
|
|
print()
|
|||
|
|
|
|||
|
|
print(f"Итого: {results['passed']} из {results['total']} тестов пройдено")
|
|||
|
|
if results['failed'] == 0:
|
|||
|
|
print("✅ Все тесты пройдены")
|
|||
|
|
else:
|
|||
|
|
print(f"❌ {results['failed']} тестов провалено")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
"""CLI интерфейс"""
|
|||
|
|
parser = argparse.ArgumentParser(
|
|||
|
|
description="Внешний Гиппокамп Montana",
|
|||
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|||
|
|
epilog="""
|
|||
|
|
Примеры:
|
|||
|
|
python hippocampus.py --view # Последние 10 мыслей
|
|||
|
|
python hippocampus.py --view 50 # Последние 50 мыслей
|
|||
|
|
python hippocampus.py --stats # Статистика памяти
|
|||
|
|
python hippocampus.py --test # Тесты детектора
|
|||
|
|
python hippocampus.py --search "маска" # Поиск по мыслям
|
|||
|
|
python hippocampus.py --export # Экспорт в Markdown
|
|||
|
|
"""
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
parser.add_argument(
|
|||
|
|
'--view', '-v',
|
|||
|
|
type=int,
|
|||
|
|
nargs='?',
|
|||
|
|
const=10,
|
|||
|
|
help='Просмотр последних N мыслей (по умолчанию 10)'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
parser.add_argument(
|
|||
|
|
'--stats', '-s',
|
|||
|
|
action='store_true',
|
|||
|
|
help='Статистика памяти'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
parser.add_argument(
|
|||
|
|
'--test', '-t',
|
|||
|
|
action='store_true',
|
|||
|
|
help='Запуск тестов детектора новизны'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
parser.add_argument(
|
|||
|
|
'--search', '-q',
|
|||
|
|
type=str,
|
|||
|
|
help='Поиск по мыслям'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
parser.add_argument(
|
|||
|
|
'--export', '-e',
|
|||
|
|
action='store_true',
|
|||
|
|
help='Экспорт памяти в Markdown'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
parser.add_argument(
|
|||
|
|
'--user', '-u',
|
|||
|
|
type=int,
|
|||
|
|
help='Фильтр по user_id'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
parser.add_argument(
|
|||
|
|
'--data-dir', '-d',
|
|||
|
|
type=str,
|
|||
|
|
help='Путь к папке данных'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
args = parser.parse_args()
|
|||
|
|
|
|||
|
|
# Создаём экземпляр гиппокампа
|
|||
|
|
hippocampus = ExternalHippocampus(data_dir=args.data_dir)
|
|||
|
|
|
|||
|
|
# Выполняем команду
|
|||
|
|
if args.view is not None:
|
|||
|
|
thoughts = hippocampus.view_stream(limit=args.view, user_id=args.user)
|
|||
|
|
print(f"Ɉ Поток мыслей Montana ({len(thoughts)} из {args.view})")
|
|||
|
|
print()
|
|||
|
|
print_thoughts(thoughts)
|
|||
|
|
|
|||
|
|
elif args.stats:
|
|||
|
|
stats = hippocampus.memory_stats(user_id=args.user)
|
|||
|
|
print_stats(stats)
|
|||
|
|
|
|||
|
|
elif args.test:
|
|||
|
|
results = hippocampus.run_tests()
|
|||
|
|
print_tests(results)
|
|||
|
|
|
|||
|
|
elif args.search:
|
|||
|
|
thoughts = hippocampus.search(args.search)
|
|||
|
|
print(f"Ɉ Поиск: \"{args.search}\" ({len(thoughts)} результатов)")
|
|||
|
|
print()
|
|||
|
|
print_thoughts(thoughts)
|
|||
|
|
|
|||
|
|
elif args.export:
|
|||
|
|
markdown = hippocampus.export_markdown(user_id=args.user)
|
|||
|
|
print(markdown)
|
|||
|
|
|
|||
|
|
else:
|
|||
|
|
parser.print_help()
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|