feat: Add presence heartbeat for Matrix online status
- matrix-gateway: POST /internal/matrix/presence/online endpoint - usePresenceHeartbeat hook with activity tracking - Auto away after 5 min inactivity - Offline on page close/visibility change - Integrated in MatrixChatRoom component
This commit is contained in:
@@ -7,10 +7,33 @@ from sqlalchemy import (
|
||||
Column, String, Text, JSON, TIMESTAMP,
|
||||
CheckConstraint, Index, Boolean, Integer
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.sql import func
|
||||
from pgvector.sqlalchemy import Vector
|
||||
import os
|
||||
|
||||
# Перевірка типу бази даних
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./memory.db")
|
||||
IS_SQLITE = "sqlite" in DATABASE_URL.lower()
|
||||
|
||||
if IS_SQLITE:
|
||||
# Для SQLite використовуємо стандартні типи
|
||||
from sqlalchemy import JSON as JSONB_TYPE
|
||||
UUID_TYPE = String # SQLite не має UUID, використовуємо String
|
||||
else:
|
||||
# Для PostgreSQL використовуємо специфічні типи
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
UUID_TYPE = UUID
|
||||
JSONB_TYPE = JSONB
|
||||
|
||||
try:
|
||||
from pgvector.sqlalchemy import Vector
|
||||
HAS_PGVECTOR = True
|
||||
except ImportError:
|
||||
HAS_PGVECTOR = False
|
||||
# Заглушка для SQLite
|
||||
class Vector:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
@@ -23,7 +46,7 @@ class UserFact(Base):
|
||||
"""
|
||||
__tablename__ = "user_facts"
|
||||
|
||||
id = Column(UUID(as_uuid=False), primary_key=True, server_default=func.gen_random_uuid())
|
||||
id = Column(UUID_TYPE(as_uuid=False) if not IS_SQLITE else String, primary_key=True, server_default=func.gen_random_uuid() if not IS_SQLITE else None)
|
||||
user_id = Column(String, nullable=False, index=True) # Без FK constraint для тестування
|
||||
team_id = Column(String, nullable=True, index=True) # Без FK constraint, оскільки teams може не існувати
|
||||
|
||||
@@ -32,18 +55,18 @@ class UserFact(Base):
|
||||
|
||||
# Значення факту (може бути текст, число, boolean, JSON)
|
||||
fact_value = Column(Text, nullable=True)
|
||||
fact_value_json = Column(JSONB, nullable=True)
|
||||
fact_value_json = Column(JSONB_TYPE, nullable=True)
|
||||
|
||||
# Метадані: джерело, впевненість, термін дії
|
||||
meta = Column(JSONB, nullable=False, server_default="{}")
|
||||
meta = Column(JSONB_TYPE, nullable=False, server_default="{}")
|
||||
|
||||
# Токен-гейт: чи залежить факт від токенів/активності
|
||||
token_gated = Column(Boolean, nullable=False, server_default="false")
|
||||
token_requirements = Column(JSONB, nullable=True) # {"token": "DAAR", "min_balance": 1}
|
||||
token_requirements = Column(JSONB_TYPE, nullable=True) # {"token": "DAAR", "min_balance": 1}
|
||||
|
||||
created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=func.now())
|
||||
updated_at = Column(TIMESTAMP(timezone=True), nullable=True, onupdate=func.now())
|
||||
expires_at = Column(TIMESTAMP(timezone=True), nullable=True) # Для тимчасових фактів
|
||||
created_at = Column(TIMESTAMP(timezone=True) if not IS_SQLITE else TIMESTAMP, nullable=False, server_default=func.now())
|
||||
updated_at = Column(TIMESTAMP(timezone=True) if not IS_SQLITE else TIMESTAMP, nullable=True, onupdate=func.now())
|
||||
expires_at = Column(TIMESTAMP(timezone=True) if not IS_SQLITE else TIMESTAMP, nullable=True) # Для тимчасових фактів
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_user_facts_user_key", "user_id", "fact_key"),
|
||||
@@ -59,7 +82,7 @@ class DialogSummary(Base):
|
||||
"""
|
||||
__tablename__ = "dialog_summaries"
|
||||
|
||||
id = Column(UUID(as_uuid=False), primary_key=True, server_default=func.gen_random_uuid())
|
||||
id = Column(UUID_TYPE(as_uuid=False) if not IS_SQLITE else String, primary_key=True, server_default=func.gen_random_uuid() if not IS_SQLITE else None)
|
||||
|
||||
# Контекст діалогу (без FK constraints для тестування)
|
||||
team_id = Column(String, nullable=False, index=True)
|
||||
@@ -68,24 +91,24 @@ class DialogSummary(Base):
|
||||
user_id = Column(String, nullable=True, index=True)
|
||||
|
||||
# Період, який охоплює підсумок
|
||||
period_start = Column(TIMESTAMP(timezone=True), nullable=False)
|
||||
period_end = Column(TIMESTAMP(timezone=True), nullable=False)
|
||||
period_start = Column(TIMESTAMP(timezone=True) if not IS_SQLITE else TIMESTAMP, nullable=False)
|
||||
period_end = Column(TIMESTAMP(timezone=True) if not IS_SQLITE else TIMESTAMP, nullable=False)
|
||||
|
||||
# Підсумок
|
||||
summary_text = Column(Text, nullable=False)
|
||||
summary_json = Column(JSONB, nullable=True) # Структуровані дані
|
||||
summary_json = Column(JSONB_TYPE, nullable=True) # Структуровані дані
|
||||
|
||||
# Статистика
|
||||
message_count = Column(Integer, nullable=False, server_default="0")
|
||||
participant_count = Column(Integer, nullable=False, server_default="0")
|
||||
|
||||
# Ключові теми/теги
|
||||
topics = Column(JSONB, nullable=True) # ["project-planning", "bug-fix", ...]
|
||||
topics = Column(JSONB_TYPE, nullable=True) # ["project-planning", "bug-fix", ...]
|
||||
|
||||
# Метадані
|
||||
meta = Column(JSONB, nullable=False, server_default="{}")
|
||||
meta = Column(JSONB_TYPE, nullable=False, server_default="{}")
|
||||
|
||||
created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=func.now())
|
||||
created_at = Column(TIMESTAMP(timezone=True) if not IS_SQLITE else TIMESTAMP, nullable=False, server_default=func.now())
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_dialog_summaries_team_period", "team_id", "period_start", "period_end"),
|
||||
@@ -101,7 +124,7 @@ class AgentMemoryEvent(Base):
|
||||
"""
|
||||
__tablename__ = "agent_memory_events"
|
||||
|
||||
id = Column(UUID(as_uuid=False), primary_key=True, server_default=func.gen_random_uuid())
|
||||
id = Column(UUID_TYPE(as_uuid=False) if not IS_SQLITE else String, primary_key=True, server_default=func.gen_random_uuid() if not IS_SQLITE else None)
|
||||
|
||||
# Без FK constraints для тестування
|
||||
agent_id = Column(String, nullable=False, index=True)
|
||||
@@ -117,9 +140,9 @@ class AgentMemoryEvent(Base):
|
||||
|
||||
# Тіло події
|
||||
body_text = Column(Text, nullable=True)
|
||||
body_json = Column(JSONB, nullable=True)
|
||||
body_json = Column(JSONB_TYPE, nullable=True)
|
||||
|
||||
created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=func.now())
|
||||
created_at = Column(TIMESTAMP(timezone=True) if not IS_SQLITE else TIMESTAMP, nullable=False, server_default=func.now())
|
||||
|
||||
__table_args__ = (
|
||||
CheckConstraint("scope IN ('short_term', 'mid_term', 'long_term')", name="ck_agent_memory_scope"),
|
||||
@@ -136,18 +159,18 @@ class AgentMemoryFactsVector(Base):
|
||||
"""
|
||||
__tablename__ = "agent_memory_facts_vector"
|
||||
|
||||
id = Column(UUID(as_uuid=False), primary_key=True, server_default=func.gen_random_uuid())
|
||||
id = Column(UUID_TYPE(as_uuid=False) if not IS_SQLITE else String, primary_key=True, server_default=func.gen_random_uuid() if not IS_SQLITE else None)
|
||||
|
||||
# Без FK constraints для тестування
|
||||
agent_id = Column(String, nullable=False, index=True)
|
||||
team_id = Column(String, nullable=False, index=True)
|
||||
|
||||
fact_text = Column(Text, nullable=False)
|
||||
embedding = Column(Vector(1536), nullable=True) # OpenAI ada-002 embedding size
|
||||
embedding = Column(Vector(1536), nullable=True) if HAS_PGVECTOR else Column(Text, nullable=True) # OpenAI ada-002 embedding size
|
||||
|
||||
meta = Column(JSONB, nullable=False, server_default="{}")
|
||||
meta = Column(JSONB_TYPE, nullable=False, server_default="{}")
|
||||
|
||||
created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=func.now())
|
||||
created_at = Column(TIMESTAMP(timezone=True) if not IS_SQLITE else TIMESTAMP, nullable=False, server_default=func.now())
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_agent_memory_facts_vector_agent_team", "agent_id", "team_id"),
|
||||
|
||||
Reference in New Issue
Block a user