feat(database): add schema bootstrap and config

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-08 00:10:42 +08:00
parent 62bf414ff2
commit 4702cc8ed2
5 changed files with 97 additions and 1 deletions

View File

@@ -104,6 +104,15 @@ class Settings(BaseSettings):
WEB_SEARCH_DEFAULT_LIMIT: int = 5 WEB_SEARCH_DEFAULT_LIMIT: int = 5
WEB_SEARCH_TIMEOUT_SECONDS: int = 10 WEB_SEARCH_TIMEOUT_SECONDS: int = 10
# === Hermes 风格升级开关 ===
ENABLE_RETROSPECTIVE: bool = True
ENABLE_SESSION_RETROSPECTIVE_SEARCH: bool = True
ENABLE_RUNTIME_SKILL_SHORTLIST: bool = True
ENABLE_LEARNING_SIGNALS: bool = True
ENABLE_SKILL_PROMOTION: bool = True
ENABLE_LEARNED_SKILL_LOADING: bool = True
ENABLE_PARALLEL_TASK_GRAPH: bool = True
settings = Settings() settings = Settings()
settings.DATABASE_URL = settings.DATABASE_URL.replace("./data", _resolve_path("./data"), 1) settings.DATABASE_URL = settings.DATABASE_URL.replace("./data", _resolve_path("./data"), 1)

View File

@@ -39,10 +39,12 @@ async def init_db():
await ensure_message_columns(conn) await ensure_message_columns(conn)
await ensure_conversation_columns(conn) await ensure_conversation_columns(conn)
await ensure_document_columns(conn) await ensure_document_columns(conn)
await ensure_memory_columns(conn)
await ensure_user_columns(conn) await ensure_user_columns(conn)
await ensure_forum_columns(conn) await ensure_forum_columns(conn)
await ensure_agent_columns(conn) await ensure_agent_columns(conn)
await ensure_skill_columns(conn) await ensure_skill_columns(conn)
await ensure_learning_artifact_tables(conn)
async def ensure_log_columns(conn): async def ensure_log_columns(conn):
@@ -115,6 +117,28 @@ async def ensure_document_columns(conn):
await conn.execute(text(ddl)) await conn.execute(text(ddl))
async def ensure_memory_columns(conn):
rows = await _get_table_info(conn, 'user_memories')
if not rows:
return
columns = {row[1] for row in rows}
required_columns = {
'frequency_count': "ALTER TABLE user_memories ADD COLUMN frequency_count INTEGER DEFAULT 0",
'emotion_tags': "ALTER TABLE user_memories ADD COLUMN emotion_tags JSON",
'importance_score': "ALTER TABLE user_memories ADD COLUMN importance_score FLOAT DEFAULT 0.5",
'importance_level': "ALTER TABLE user_memories ADD COLUMN importance_level VARCHAR(20) DEFAULT 'medium'",
'associated_topics': "ALTER TABLE user_memories ADD COLUMN associated_topics JSON",
'decay_score': "ALTER TABLE user_memories ADD COLUMN decay_score FLOAT DEFAULT 1.0",
'is_archived': "ALTER TABLE user_memories ADD COLUMN is_archived BOOLEAN DEFAULT 0",
'last_accessed_at': "ALTER TABLE user_memories ADD COLUMN last_accessed_at DATETIME",
'archive_at': "ALTER TABLE user_memories ADD COLUMN archive_at DATETIME",
}
for column, ddl in required_columns.items():
if column not in columns:
await conn.execute(text(ddl))
async def ensure_user_columns(conn): async def ensure_user_columns(conn):
rows = await _get_table_info(conn, 'users') rows = await _get_table_info(conn, 'users')
if not rows: if not rows:
@@ -181,6 +205,14 @@ async def ensure_skill_columns(conn):
'output_format': "ALTER TABLE skills ADD COLUMN output_format TEXT", 'output_format': "ALTER TABLE skills ADD COLUMN output_format TEXT",
'is_builtin': "ALTER TABLE skills ADD COLUMN is_builtin BOOLEAN DEFAULT 0 NOT NULL", 'is_builtin': "ALTER TABLE skills ADD COLUMN is_builtin BOOLEAN DEFAULT 0 NOT NULL",
'team_id': "ALTER TABLE skills ADD COLUMN team_id VARCHAR(36)", 'team_id': "ALTER TABLE skills ADD COLUMN team_id VARCHAR(36)",
'status': "ALTER TABLE skills ADD COLUMN status VARCHAR(20) DEFAULT 'active' NOT NULL",
'scope': "ALTER TABLE skills ADD COLUMN scope JSON DEFAULT '[]' NOT NULL",
'effectiveness': "ALTER TABLE skills ADD COLUMN effectiveness FLOAT DEFAULT 0.0 NOT NULL",
'review_after': "ALTER TABLE skills ADD COLUMN review_after DATETIME",
'candidate_count': "ALTER TABLE skills ADD COLUMN candidate_count INTEGER DEFAULT 0 NOT NULL",
'candidate_source_hashes': "ALTER TABLE skills ADD COLUMN candidate_source_hashes JSON DEFAULT '[]' NOT NULL",
'activation_count': "ALTER TABLE skills ADD COLUMN activation_count INTEGER DEFAULT 0 NOT NULL",
'last_activated_at': "ALTER TABLE skills ADD COLUMN last_activated_at DATETIME",
} }
for column, ddl in required_columns.items(): for column, ddl in required_columns.items():
if column not in columns: if column not in columns:
@@ -205,6 +237,48 @@ async def ensure_skill_columns(conn):
) )
async def ensure_learning_artifact_tables(conn):
await conn.execute(
text(
"""
CREATE TABLE IF NOT EXISTS learning_artifacts (
id VARCHAR(36) PRIMARY KEY,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
user_id VARCHAR(36) NOT NULL,
conversation_id VARCHAR(36) NOT NULL,
retrospective_id VARCHAR(36),
artifact_type VARCHAR(32) NOT NULL,
artifact_key VARCHAR(128),
summary_text TEXT NOT NULL,
payload JSON NOT NULL,
recorded_at DATETIME NOT NULL
)
"""
)
)
await conn.execute(
text(
"CREATE INDEX IF NOT EXISTS ix_learning_artifacts_user_id ON learning_artifacts (user_id)"
)
)
await conn.execute(
text(
"CREATE INDEX IF NOT EXISTS ix_learning_artifacts_conversation_id ON learning_artifacts (conversation_id)"
)
)
await conn.execute(
text(
"CREATE INDEX IF NOT EXISTS ix_learning_artifacts_retrospective_id ON learning_artifacts (retrospective_id)"
)
)
await conn.execute(
text(
"CREATE INDEX IF NOT EXISTS ix_learning_artifacts_artifact_type ON learning_artifacts (artifact_type)"
)
)
async def _backfill_usernames(conn): async def _backfill_usernames(conn):
result = await conn.execute(text("SELECT id, email, username FROM users ORDER BY created_at, id")) result = await conn.execute(text("SELECT id, email, username FROM users ORDER BY created_at, id"))
users = result.fetchall() users = result.fetchall()

View File

@@ -7,6 +7,7 @@ from app.models.forum import ForumPost, ForumReply
from app.models.agent import Agent, AgentMessage from app.models.agent import Agent, AgentMessage
from app.models.conversation import Conversation, Message from app.models.conversation import Conversation, Message
from app.models.knowledge_graph import KGNode, KGEdge from app.models.knowledge_graph import KGNode, KGEdge
from app.models.learning import LearningArtifactRecord, SessionRetrospectiveRecord
from app.models.memory import MemorySummary, UserMemory from app.models.memory import MemorySummary, UserMemory
from app.models.brain import ( from app.models.brain import (
BrainEvent, BrainEvent,
@@ -20,6 +21,7 @@ from app.models.brain import (
from app.models.todo import DailyTodo, TodoSource from app.models.todo import DailyTodo, TodoSource
from app.models.reminder import Reminder, ReminderStatus from app.models.reminder import Reminder, ReminderStatus
from app.models.goal import Goal, GoalStatus from app.models.goal import Goal, GoalStatus
from app.models.skill import Skill
from app.models.log import Log, LogType, LogLevel from app.models.log import Log, LogType, LogLevel
__all__ = [ __all__ = [
@@ -38,6 +40,8 @@ __all__ = [
"Message", "Message",
"KGNode", "KGNode",
"KGEdge", "KGEdge",
"LearningArtifactRecord",
"SessionRetrospectiveRecord",
"MemorySummary", "MemorySummary",
"UserMemory", "UserMemory",
"BrainEvent", "BrainEvent",
@@ -53,6 +57,7 @@ __all__ = [
"ReminderStatus", "ReminderStatus",
"Goal", "Goal",
"GoalStatus", "GoalStatus",
"Skill",
"Log", "Log",
"LogType", "LogType",
"LogLevel", "LogLevel",

View File

@@ -1,4 +1,4 @@
from sqlalchemy import Column, String, Text, Boolean, JSON, ForeignKey from sqlalchemy import Column, String, Text, Boolean, JSON, ForeignKey, Float, Integer, DateTime
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app.models.base import BaseModel from app.models.base import BaseModel
@@ -17,6 +17,14 @@ class Skill(BaseModel):
is_builtin = Column(Boolean, default=False, nullable=False) is_builtin = Column(Boolean, default=False, nullable=False)
team_id = Column(String(36), ForeignKey("users.id"), nullable=True) team_id = Column(String(36), ForeignKey("users.id"), nullable=True)
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
status = Column(String(20), default="active", nullable=False, index=True) # candidate/shadow/active/deprecated/retired
scope = Column(JSON, default=list, nullable=False)
effectiveness = Column(Float, default=0.0, nullable=False)
review_after = Column(DateTime, nullable=True)
candidate_count = Column(Integer, default=0, nullable=False)
candidate_source_hashes = Column(JSON, default=list, nullable=False)
activation_count = Column(Integer, default=0, nullable=False)
last_activated_at = Column(DateTime, nullable=True)
owner_id = Column(String(36), ForeignKey("users.id"), nullable=False) owner_id = Column(String(36), ForeignKey("users.id"), nullable=False)
owner = relationship("User", foreign_keys=[owner_id]) owner = relationship("User", foreign_keys=[owner_id])

Binary file not shown.