mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
feat: add Telegram DM topic-mode sessions
This commit is contained in:
parent
0ce1b9fe20
commit
d6615d8ec7
8 changed files with 1890 additions and 18 deletions
|
|
@ -35,6 +35,7 @@ class TestSessionLifecycle:
|
|||
assert session["model"] == "test-model"
|
||||
assert session["ended_at"] is None
|
||||
|
||||
|
||||
def test_get_nonexistent_session(self, db):
|
||||
assert db.get_session("nonexistent") is None
|
||||
|
||||
|
|
@ -1421,6 +1422,242 @@ class TestSchemaInit:
|
|||
columns = {row[1] for row in cursor.fetchall()}
|
||||
assert "title" in columns
|
||||
|
||||
def test_topic_mode_schema_is_not_auto_migrated_on_open(self, tmp_path):
|
||||
"""Opening an old DB should not add topic-mode columns until /topic opts in.
|
||||
|
||||
The gateway must remain rollback-safe: simply upgrading Hermes and starting
|
||||
the old bot should not eagerly mutate the state DB for this feature.
|
||||
"""
|
||||
old_db = tmp_path / "old.db"
|
||||
import sqlite3
|
||||
|
||||
conn = sqlite3.connect(old_db)
|
||||
conn.executescript(
|
||||
"""
|
||||
CREATE TABLE schema_version (version INTEGER NOT NULL);
|
||||
INSERT INTO schema_version VALUES (11);
|
||||
CREATE TABLE sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
source TEXT NOT NULL,
|
||||
user_id TEXT,
|
||||
model TEXT,
|
||||
model_config TEXT,
|
||||
system_prompt TEXT,
|
||||
parent_session_id TEXT,
|
||||
started_at REAL NOT NULL,
|
||||
ended_at REAL,
|
||||
end_reason TEXT,
|
||||
message_count INTEGER DEFAULT 0,
|
||||
tool_call_count INTEGER DEFAULT 0,
|
||||
input_tokens INTEGER DEFAULT 0,
|
||||
output_tokens INTEGER DEFAULT 0,
|
||||
cache_read_tokens INTEGER DEFAULT 0,
|
||||
cache_write_tokens INTEGER DEFAULT 0,
|
||||
reasoning_tokens INTEGER DEFAULT 0,
|
||||
billing_provider TEXT,
|
||||
billing_base_url TEXT,
|
||||
billing_mode TEXT,
|
||||
estimated_cost_usd REAL,
|
||||
actual_cost_usd REAL,
|
||||
cost_status TEXT,
|
||||
cost_source TEXT,
|
||||
pricing_version TEXT,
|
||||
title TEXT,
|
||||
api_call_count INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (parent_session_id) REFERENCES sessions(id)
|
||||
);
|
||||
CREATE TABLE messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL REFERENCES sessions(id),
|
||||
role TEXT NOT NULL,
|
||||
content TEXT,
|
||||
tool_call_id TEXT,
|
||||
tool_calls TEXT,
|
||||
tool_name TEXT,
|
||||
timestamp REAL NOT NULL,
|
||||
token_count INTEGER,
|
||||
finish_reason TEXT,
|
||||
reasoning TEXT,
|
||||
reasoning_content TEXT,
|
||||
reasoning_details TEXT,
|
||||
codex_reasoning_items TEXT,
|
||||
codex_message_items TEXT
|
||||
);
|
||||
"""
|
||||
)
|
||||
conn.close()
|
||||
|
||||
db = SessionDB(db_path=old_db)
|
||||
cursor = db._conn.execute("PRAGMA table_info(sessions)")
|
||||
columns = {row[1] for row in cursor.fetchall()}
|
||||
assert {"chat_id", "chat_type", "thread_id", "session_key"}.isdisjoint(columns)
|
||||
db.close()
|
||||
|
||||
def test_apply_telegram_topic_migration_creates_topic_tables_explicitly(self, tmp_path):
|
||||
"""The /topic opt-in path owns the DB migration for Telegram topic mode."""
|
||||
old_db = tmp_path / "old.db"
|
||||
import sqlite3
|
||||
|
||||
conn = sqlite3.connect(old_db)
|
||||
conn.executescript(
|
||||
"""
|
||||
CREATE TABLE schema_version (version INTEGER NOT NULL);
|
||||
INSERT INTO schema_version VALUES (11);
|
||||
CREATE TABLE sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
source TEXT NOT NULL,
|
||||
user_id TEXT,
|
||||
model TEXT,
|
||||
model_config TEXT,
|
||||
system_prompt TEXT,
|
||||
parent_session_id TEXT,
|
||||
started_at REAL NOT NULL,
|
||||
ended_at REAL,
|
||||
end_reason TEXT,
|
||||
message_count INTEGER DEFAULT 0,
|
||||
tool_call_count INTEGER DEFAULT 0,
|
||||
input_tokens INTEGER DEFAULT 0,
|
||||
output_tokens INTEGER DEFAULT 0,
|
||||
cache_read_tokens INTEGER DEFAULT 0,
|
||||
cache_write_tokens INTEGER DEFAULT 0,
|
||||
reasoning_tokens INTEGER DEFAULT 0,
|
||||
billing_provider TEXT,
|
||||
billing_base_url TEXT,
|
||||
billing_mode TEXT,
|
||||
estimated_cost_usd REAL,
|
||||
actual_cost_usd REAL,
|
||||
cost_status TEXT,
|
||||
cost_source TEXT,
|
||||
pricing_version TEXT,
|
||||
title TEXT,
|
||||
api_call_count INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (parent_session_id) REFERENCES sessions(id)
|
||||
);
|
||||
CREATE TABLE messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL REFERENCES sessions(id),
|
||||
role TEXT NOT NULL,
|
||||
content TEXT,
|
||||
tool_call_id TEXT,
|
||||
tool_calls TEXT,
|
||||
tool_name TEXT,
|
||||
timestamp REAL NOT NULL,
|
||||
token_count INTEGER,
|
||||
finish_reason TEXT,
|
||||
reasoning TEXT,
|
||||
reasoning_content TEXT,
|
||||
reasoning_details TEXT,
|
||||
codex_reasoning_items TEXT,
|
||||
codex_message_items TEXT
|
||||
);
|
||||
"""
|
||||
)
|
||||
conn.close()
|
||||
|
||||
db = SessionDB(db_path=old_db)
|
||||
db.apply_telegram_topic_migration()
|
||||
|
||||
tables = {
|
||||
row[0]
|
||||
for row in db._conn.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type = 'table'"
|
||||
).fetchall()
|
||||
}
|
||||
assert "telegram_dm_topic_mode" in tables
|
||||
assert "telegram_dm_topic_bindings" in tables
|
||||
assert db.get_meta("telegram_dm_topic_schema_version") == "1"
|
||||
db.close()
|
||||
|
||||
def test_telegram_topic_binding_roundtrip_requires_explicit_schema(self, tmp_path):
|
||||
db = SessionDB(db_path=tmp_path / "state.db")
|
||||
db.create_session(
|
||||
session_id="topic-session",
|
||||
source="telegram",
|
||||
user_id="208214988",
|
||||
)
|
||||
|
||||
assert db.get_telegram_topic_binding(chat_id="208214988", thread_id="17585") is None
|
||||
|
||||
db.bind_telegram_topic(
|
||||
chat_id="208214988",
|
||||
thread_id="17585",
|
||||
user_id="208214988",
|
||||
session_key="telegram:dm:208214988:thread:17585",
|
||||
session_id="topic-session",
|
||||
)
|
||||
|
||||
binding = db.get_telegram_topic_binding(chat_id="208214988", thread_id="17585")
|
||||
assert binding is not None
|
||||
assert binding["chat_id"] == "208214988"
|
||||
assert binding["thread_id"] == "17585"
|
||||
assert binding["user_id"] == "208214988"
|
||||
assert binding["session_key"] == "telegram:dm:208214988:thread:17585"
|
||||
assert binding["session_id"] == "topic-session"
|
||||
assert db.get_meta("telegram_dm_topic_schema_version") == "1"
|
||||
db.close()
|
||||
|
||||
def test_telegram_topic_binding_refuses_to_relink_session_to_another_topic(self, tmp_path):
|
||||
db = SessionDB(db_path=tmp_path / "state.db")
|
||||
db.create_session(
|
||||
session_id="topic-session",
|
||||
source="telegram",
|
||||
user_id="208214988",
|
||||
)
|
||||
db.bind_telegram_topic(
|
||||
chat_id="208214988",
|
||||
thread_id="17585",
|
||||
user_id="208214988",
|
||||
session_key="key-17585",
|
||||
session_id="topic-session",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="already linked"):
|
||||
db.bind_telegram_topic(
|
||||
chat_id="208214988",
|
||||
thread_id="99999",
|
||||
user_id="208214988",
|
||||
session_key="key-99999",
|
||||
session_id="topic-session",
|
||||
)
|
||||
db.close()
|
||||
|
||||
def test_list_unlinked_telegram_sessions_for_user_excludes_bound_and_other_users(self, tmp_path):
|
||||
db = SessionDB(db_path=tmp_path / "state.db")
|
||||
db.create_session(
|
||||
session_id="old-unlinked",
|
||||
source="telegram",
|
||||
user_id="208214988",
|
||||
)
|
||||
db.set_session_title("old-unlinked", "Old research")
|
||||
db.append_message("old-unlinked", "user", "first prompt")
|
||||
db.create_session(
|
||||
session_id="already-linked",
|
||||
source="telegram",
|
||||
user_id="208214988",
|
||||
)
|
||||
db.bind_telegram_topic(
|
||||
chat_id="208214988",
|
||||
thread_id="17585",
|
||||
user_id="208214988",
|
||||
session_key="key-17585",
|
||||
session_id="already-linked",
|
||||
)
|
||||
db.create_session(
|
||||
session_id="other-user",
|
||||
source="telegram",
|
||||
user_id="someone-else",
|
||||
)
|
||||
|
||||
sessions = db.list_unlinked_telegram_sessions_for_user(
|
||||
chat_id="208214988",
|
||||
user_id="208214988",
|
||||
)
|
||||
|
||||
assert [s["id"] for s in sessions] == ["old-unlinked"]
|
||||
assert sessions[0]["title"] == "Old research"
|
||||
assert sessions[0]["preview"] == "first prompt"
|
||||
db.close()
|
||||
|
||||
def test_migration_from_v2(self, tmp_path):
|
||||
"""Simulate a v2 database and verify migration adds title column."""
|
||||
import sqlite3
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue