diff --git a/backend/alembic/versions/001_initial_schema.py b/backend/alembic/versions/001_initial_schema.py deleted file mode 100644 index bfbaa47..0000000 --- a/backend/alembic/versions/001_initial_schema.py +++ /dev/null @@ -1,1225 +0,0 @@ -"""initial_schema - -Revision ID: 001 -Revises: -Create Date: 2026-07-09 19:36:42.745763 - -""" - -from collections.abc import Sequence - -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "001" -down_revision: str | Sequence[str] | None = None -branch_labels: str | Sequence[str] | None = None -depends_on: str | Sequence[str] | None = None - - -def upgrade() -> None: - """Upgrade schema.""" - # 天工阁角色名册 - op.create_table( - "agent_roster", - sa.Column("role", sa.String(length=80), nullable=False), - sa.Column("name", sa.String(length=120), nullable=False), - sa.Column("responsibility", sa.Text(), nullable=False), - sa.Column("enabled", sa.Boolean(), nullable=False), - sa.Column("last_active_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint("role"), - ) - # 人物主表 - op.create_table( - "characters", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("name", sa.String(length=120), nullable=False), - sa.Column("display_name", sa.String(length=120), nullable=True), - sa.Column("status", sa.String(length=40), nullable=False), - sa.Column("language", sa.String(length=20), nullable=False), - sa.Column("profile_json", sa.Text(), nullable=False), - sa.Column("model_policy_json", sa.Text(), nullable=False), - sa.Column("safety_policy_id", sa.String(length=80), nullable=True), - sa.Column("current_personality_version_id", sa.String(length=80), nullable=True), - sa.Column("current_mood_id", sa.String(length=80), nullable=True), - sa.Column("privacy_level", sa.String(length=40), nullable=False), - sa.Column("owner_scope", sa.String(length=80), nullable=False), - sa.Column("is_system_template", sa.Boolean(), nullable=False), - sa.Column("source_template_id", sa.String(length=80), nullable=True), - sa.Column("template_key", sa.String(length=80), nullable=True), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("activated_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("archived_at", sa.DateTime(timezone=True), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_characters_is_system_template"), "characters", ["is_system_template"], unique=False - ) - op.create_index(op.f("ix_characters_owner_scope"), "characters", ["owner_scope"], unique=False) - op.create_index( - op.f("ix_characters_source_template_id"), "characters", ["source_template_id"], unique=False - ) - op.create_index(op.f("ix_characters_status"), "characters", ["status"], unique=False) - op.create_index( - op.f("ix_characters_template_key"), "characters", ["template_key"], unique=False - ) - # 生成偏好 - op.create_table( - "generation_preferences", - sa.Column("owner_scope", sa.String(length=80), nullable=False), - sa.Column("default_model_hint", sa.String(length=120), nullable=False), - sa.Column("temperature", sa.Float(), nullable=False), - sa.Column("top_p", sa.Float(), nullable=False), - sa.Column("max_output_tokens", sa.Integer(), nullable=False), - sa.Column("streaming", sa.Boolean(), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint("owner_scope"), - ) - # 幂等键 - op.create_table( - "idempotency_keys", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("key", sa.String(length=200), nullable=False), - sa.Column("scope", sa.String(length=120), nullable=False), - sa.Column("method", sa.String(length=20), nullable=False), - sa.Column("path", sa.String(length=500), nullable=False), - sa.Column("request_hash", sa.String(length=128), nullable=False), - sa.Column("status", sa.String(length=40), nullable=False), - sa.Column("response_status_code", sa.Integer(), nullable=False), - sa.Column("response_body_json", sa.Text(), nullable=False), - sa.Column("resource_type", sa.String(length=80), nullable=True), - sa.Column("resource_id", sa.String(length=120), nullable=True), - sa.Column("trace_id", sa.String(length=80), nullable=True), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("expires_at", sa.DateTime(timezone=True), nullable=True), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("scope", "key", name="uq_idempotency_scope_key"), - ) - op.create_index( - op.f("ix_idempotency_keys_expires_at"), "idempotency_keys", ["expires_at"], unique=False - ) - op.create_index(op.f("ix_idempotency_keys_key"), "idempotency_keys", ["key"], unique=False) - op.create_index(op.f("ix_idempotency_keys_scope"), "idempotency_keys", ["scope"], unique=False) - op.create_index( - op.f("ix_idempotency_keys_trace_id"), "idempotency_keys", ["trace_id"], unique=False - ) - # MCP 服务器注册 - op.create_table( - "mcp_servers", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("name", sa.String(length=120), nullable=False), - sa.Column("transport", sa.String(length=40), nullable=False), - sa.Column("endpoint_url", sa.String(length=500), nullable=True), - sa.Column("auth_token", sa.String(length=500), nullable=True), - sa.Column("command", sa.String(length=500), nullable=True), - sa.Column("args_json", sa.Text(), nullable=True), - sa.Column("env_json", sa.Text(), nullable=True), - sa.Column("cwd", sa.String(length=500), nullable=True), - sa.Column("status", sa.String(length=40), nullable=False), - sa.Column("tools_json", sa.Text(), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("name"), - ) - op.create_index(op.f("ix_mcp_servers_status"), "mcp_servers", ["status"], unique=False) - # 模型端点覆盖 - op.create_table( - "model_endpoint_overrides", - sa.Column("logical_model_name", sa.String(length=120), nullable=False), - sa.Column("endpoint_name", sa.String(length=120), nullable=False), - sa.Column("base_url", sa.String(length=500), nullable=True), - sa.Column("api_key", sa.String(length=500), nullable=True), - sa.Column("status", sa.String(length=40), nullable=True), - sa.Column("weight", sa.Integer(), nullable=True), - sa.Column("priority", sa.Integer(), nullable=True), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint("logical_model_name", "endpoint_name"), - ) - # 司命待确认记录 - op.create_table( - "pending_confirmations", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("tool", sa.String(length=120), nullable=False), - sa.Column("input_json", sa.Text(), nullable=False), - sa.Column("risk_level", sa.String(length=40), nullable=False), - sa.Column("reason", sa.Text(), nullable=False), - sa.Column("run_id", sa.String(length=80), nullable=True), - sa.Column("character_id", sa.String(length=80), nullable=True), - sa.Column("trace_id", sa.String(length=80), nullable=True), - sa.Column("status", sa.String(length=40), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("resolved_at", sa.DateTime(timezone=True), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_pending_confirmations_character_id"), - "pending_confirmations", - ["character_id"], - unique=False, - ) - op.create_index( - op.f("ix_pending_confirmations_run_id"), "pending_confirmations", ["run_id"], unique=False - ) - op.create_index( - op.f("ix_pending_confirmations_status"), "pending_confirmations", ["status"], unique=False - ) - # 烛照感知通道 - op.create_table( - "perception_channels", - sa.Column("channel", sa.String(length=80), nullable=False), - sa.Column("name", sa.String(length=120), nullable=False), - sa.Column("description", sa.Text(), nullable=False), - sa.Column("enabled", sa.Boolean(), nullable=False), - sa.Column("consent_status", sa.String(length=40), nullable=False), - sa.Column("privacy_level", sa.String(length=40), nullable=False), - sa.Column("risk_level", sa.String(length=20), nullable=False), - sa.Column("last_active_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint("channel"), - ) - # Skill 能力包 - op.create_table( - "skills", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("name", sa.String(length=120), nullable=False), - sa.Column("description", sa.Text(), nullable=False), - sa.Column("trigger", sa.Text(), nullable=False), - sa.Column("prompt_fragment", sa.Text(), nullable=False), - sa.Column("tool_bindings_json", sa.Text(), nullable=False), - sa.Column("risk_level", sa.String(length=8), nullable=False), - sa.Column("status", sa.String(length=40), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("name"), - ) - op.create_index(op.f("ix_skills_status"), "skills", ["status"], unique=False) - # 工具安全策略 - op.create_table( - "tool_policies", - sa.Column("name", sa.String(length=80), nullable=False), - sa.Column("description", sa.Text(), nullable=False), - sa.Column("risk_level", sa.String(length=20), nullable=False), - sa.Column("permission_policy", sa.String(length=20), nullable=False), - sa.Column("side_effect", sa.Boolean(), nullable=False), - sa.Column("audit_required", sa.Boolean(), nullable=False), - sa.Column("source", sa.String(length=20), nullable=False), - sa.Column("enabled", sa.Boolean(), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint("name"), - ) - # 用户 - op.create_table( - "users", - sa.Column("id", sa.String(length=40), nullable=False), - sa.Column("username", sa.String(length=80), nullable=False), - sa.Column("email", sa.String(length=255), nullable=False), - sa.Column("password_hash", sa.String(length=255), nullable=False), - sa.Column("is_active", sa.Boolean(), nullable=False), - sa.Column("is_admin", sa.Boolean(), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("email"), - ) - op.create_index(op.f("ix_users_username"), "users", ["username"], unique=True) - # 河图世界实体 - op.create_table( - "world_entities", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("type", sa.String(length=40), nullable=False), - sa.Column("name", sa.String(length=200), nullable=False), - sa.Column("attributes_json", sa.Text(), nullable=False), - sa.Column("state_type", sa.String(length=40), nullable=False), - sa.Column("confidence", sa.Float(), nullable=False), - sa.Column("sources_json", sa.Text(), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index(op.f("ix_world_entities_type"), "world_entities", ["type"], unique=False) - # 大周天运行记录 - op.create_table( - "big_cycles", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("character_id", sa.String(length=80), nullable=False), - sa.Column("trigger", sa.String(length=40), nullable=False), - sa.Column("status", sa.String(length=40), nullable=False), - sa.Column("phase", sa.String(length=40), nullable=True), - sa.Column("memory_count_at_trigger", sa.Integer(), nullable=False), - sa.Column("evaluation_snapshot_json", sa.Text(), nullable=True), - sa.Column("summary", sa.Text(), nullable=True), - sa.Column("error_code", sa.String(length=80), nullable=True), - sa.Column("error_message", sa.Text(), nullable=True), - sa.Column("started_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_big_cycles_character_id"), "big_cycles", ["character_id"], unique=False - ) - op.create_index( - op.f("ix_big_cycles_completed_at"), "big_cycles", ["completed_at"], unique=False - ) - op.create_index(op.f("ix_big_cycles_status"), "big_cycles", ["status"], unique=False) - # 灵核情绪状态 - op.create_table( - "character_emotion_states", - sa.Column("character_id", sa.String(length=80), nullable=False), - sa.Column("mood", sa.String(length=40), nullable=False), - sa.Column("intensity", sa.Float(), nullable=False), - sa.Column("baseline_mood", sa.String(length=40), nullable=False), - sa.Column("baseline_intensity", sa.Float(), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("last_trigger_event_id", sa.String(length=80), nullable=True), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.PrimaryKeyConstraint("character_id"), - ) - # 资料导入任务 - op.create_table( - "character_import_jobs", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("character_id", sa.String(length=80), nullable=False), - sa.Column("material_type", sa.String(length=40), nullable=False), - sa.Column("source_type", sa.String(length=40), nullable=False), - sa.Column("title", sa.String(length=200), nullable=True), - sa.Column("status", sa.String(length=40), nullable=False), - sa.Column("format", sa.String(length=40), nullable=False), - sa.Column("media_type", sa.String(length=40), nullable=True), - sa.Column("mime_type", sa.String(length=80), nullable=True), - sa.Column("source_description", sa.Text(), nullable=True), - sa.Column("speaker_label", sa.String(length=120), nullable=True), - sa.Column("consent_status", sa.String(length=40), nullable=False), - sa.Column("usage_scope", sa.String(length=40), nullable=False), - sa.Column("checksum", sa.String(length=120), nullable=True), - sa.Column("item_count", sa.Integer(), nullable=False), - sa.Column("storage_path", sa.String(length=500), nullable=True), - sa.Column("raw_content", sa.Text(), nullable=True), - sa.Column("error_code", sa.String(length=80), nullable=True), - sa.Column("error_message", sa.Text(), nullable=True), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_character_import_jobs_character_id"), - "character_import_jobs", - ["character_id"], - unique=False, - ) - op.create_index( - op.f("ix_character_import_jobs_checksum"), - "character_import_jobs", - ["checksum"], - unique=False, - ) - op.create_index( - op.f("ix_character_import_jobs_material_type"), - "character_import_jobs", - ["material_type"], - unique=False, - ) - op.create_index( - op.f("ix_character_import_jobs_status"), "character_import_jobs", ["status"], unique=False - ) - # 人物视角世界实体 - op.create_table( - "character_world_entities", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("character_id", sa.String(length=80), nullable=False), - sa.Column("type", sa.String(length=40), nullable=False), - sa.Column("name", sa.String(length=200), nullable=False), - sa.Column("attributes_json", sa.Text(), nullable=False), - sa.Column("state_type", sa.String(length=40), nullable=False), - sa.Column("status", sa.String(length=40), nullable=False), - sa.Column("confidence", sa.Float(), nullable=False), - sa.Column("evidence_ids_json", sa.Text(), nullable=False), - sa.Column("sources_json", sa.Text(), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_character_world_entities_character_id"), - "character_world_entities", - ["character_id"], - unique=False, - ) - op.create_index( - op.f("ix_character_world_entities_status"), - "character_world_entities", - ["status"], - unique=False, - ) - op.create_index( - op.f("ix_character_world_entities_type"), "character_world_entities", ["type"], unique=False - ) - # 对话会话 - op.create_table( - "chat_sessions", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("character_id", sa.String(length=80), nullable=False), - sa.Column("title", sa.String(length=200), nullable=True), - sa.Column("status", sa.String(length=40), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_chat_sessions_character_id"), "chat_sessions", ["character_id"], unique=False - ) - op.create_index(op.f("ix_chat_sessions_status"), "chat_sessions", ["status"], unique=False) - # 太一目标 - op.create_table( - "goals", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("character_id", sa.String(length=80), nullable=True), - sa.Column("title", sa.String(length=300), nullable=False), - sa.Column("description", sa.Text(), nullable=False), - sa.Column("level", sa.String(length=40), nullable=False), - sa.Column("type", sa.String(length=40), nullable=False), - sa.Column("status", sa.String(length=40), nullable=False), - sa.Column("priority", sa.Integer(), nullable=False), - sa.Column("progress", sa.Float(), nullable=False), - sa.Column("constraints_json", sa.Text(), nullable=False), - sa.Column("parent_id", sa.String(length=80), nullable=True), - sa.Column("success_criteria_json", sa.Text(), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.ForeignKeyConstraint( - ["parent_id"], - ["goals.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index(op.f("ix_goals_character_id"), "goals", ["character_id"], unique=False) - op.create_index(op.f("ix_goals_level"), "goals", ["level"], unique=False) - op.create_index(op.f("ix_goals_status"), "goals", ["status"], unique=False) - # 息壤记忆 - op.create_table( - "memories", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("character_id", sa.String(length=80), nullable=False), - sa.Column("type", sa.String(length=60), nullable=False), - sa.Column("title", sa.String(length=200), nullable=False), - sa.Column("content", sa.Text(), nullable=False), - sa.Column("source", sa.String(length=80), nullable=False), - sa.Column("confidence", sa.Float(), nullable=False), - sa.Column("importance", sa.Float(), nullable=False), - sa.Column("sensitivity", sa.String(length=40), nullable=False), - sa.Column("evidence_ids_json", sa.Text(), nullable=False), - sa.Column("status", sa.String(length=40), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("expires_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("temporal_context_json", sa.Text(), server_default="{}", nullable=False), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index(op.f("ix_memories_character_id"), "memories", ["character_id"], unique=False) - op.create_index(op.f("ix_memories_status"), "memories", ["status"], unique=False) - # 人格版本 - op.create_table( - "personality_versions", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("character_id", sa.String(length=80), nullable=False), - sa.Column("version", sa.Integer(), nullable=False), - sa.Column("identity_profile", sa.Text(), nullable=False), - sa.Column("role_destiny", sa.Text(), nullable=False), - sa.Column("personality_seed", sa.Text(), nullable=False), - sa.Column("traits", sa.Text(), nullable=False), - sa.Column("expression_style", sa.Text(), nullable=False), - sa.Column("values", sa.Text(), nullable=False), - sa.Column("relationship_pattern", sa.Text(), nullable=False), - sa.Column("emotion_baseline", sa.Text(), nullable=False), - sa.Column("reason", sa.Text(), nullable=False), - sa.Column("evidence_ids_json", sa.Text(), nullable=False), - sa.Column("created_by", sa.String(length=80), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("rolled_back_at", sa.DateTime(timezone=True), nullable=True), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_personality_versions_character_id"), - "personality_versions", - ["character_id"], - unique=False, - ) - # 辰正纪念日 - op.create_table( - "time_anniversaries", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("character_id", sa.String(length=80), nullable=False), - sa.Column("name", sa.String(length=200), nullable=False), - sa.Column("calendar", sa.String(length=20), nullable=False), - sa.Column("date", sa.String(length=20), nullable=False), - sa.Column("recurs_yearly", sa.Boolean(), nullable=False), - sa.Column("kind", sa.String(length=40), nullable=False), - sa.Column("subject_type", sa.String(length=40), nullable=False), - sa.Column("source", sa.String(length=40), nullable=False), - sa.Column("metadata_json", sa.Text(), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_time_anniversaries_character_id"), - "time_anniversaries", - ["character_id"], - unique=False, - ) - # 档案导入候选 - op.create_table( - "character_profile_candidates", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("character_id", sa.String(length=80), nullable=False), - sa.Column("import_job_id", sa.String(length=80), nullable=True), - sa.Column("field", sa.String(length=60), nullable=False), - sa.Column("value", sa.Text(), nullable=False), - sa.Column("confidence", sa.Float(), nullable=False), - sa.Column("evidence_ids_json", sa.Text(), nullable=False), - sa.Column("status", sa.String(length=40), nullable=False), - sa.Column("reason", sa.Text(), nullable=True), - sa.Column("reviewed_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.ForeignKeyConstraint( - ["import_job_id"], - ["character_import_jobs.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_character_profile_candidates_character_id"), - "character_profile_candidates", - ["character_id"], - unique=False, - ) - op.create_index( - op.f("ix_character_profile_candidates_import_job_id"), - "character_profile_candidates", - ["import_job_id"], - unique=False, - ) - op.create_index( - op.f("ix_character_profile_candidates_status"), - "character_profile_candidates", - ["status"], - unique=False, - ) - # 大周天内驱力·好奇问题 - op.create_table( - "curiosity_questions", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("character_id", sa.String(length=80), nullable=False), - sa.Column("cycle_id", sa.String(length=80), nullable=False), - sa.Column("question", sa.Text(), nullable=False), - sa.Column("source", sa.String(length=60), nullable=False), - sa.Column("related_entities_json", sa.Text(), nullable=False), - sa.Column("expected_value", sa.Float(), nullable=False), - sa.Column("urgency", sa.Float(), nullable=False), - sa.Column("risk_level", sa.String(length=8), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.ForeignKeyConstraint( - ["cycle_id"], - ["big_cycles.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_curiosity_questions_character_id"), - "curiosity_questions", - ["character_id"], - unique=False, - ) - op.create_index( - op.f("ix_curiosity_questions_cycle_id"), "curiosity_questions", ["cycle_id"], unique=False - ) - # 演化候选 - op.create_table( - "evolution_candidates", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("character_id", sa.String(length=80), nullable=False), - sa.Column("cycle_id", sa.String(length=80), nullable=False), - sa.Column("type", sa.String(length=60), nullable=False), - sa.Column("source", sa.String(length=60), nullable=False), - sa.Column("level", sa.String(length=8), nullable=False), - sa.Column("scope", sa.String(length=40), nullable=False), - sa.Column("title", sa.String(length=200), nullable=False), - sa.Column("proposal", sa.Text(), nullable=False), - sa.Column("delta_json", sa.Text(), nullable=False), - sa.Column("evidence_ids_json", sa.Text(), nullable=False), - sa.Column("confidence", sa.Float(), nullable=False), - sa.Column("benefit_score", sa.Float(), nullable=False), - sa.Column("risk_score", sa.Float(), nullable=False), - sa.Column("drift_score", sa.Float(), nullable=False), - sa.Column("auto_apply_allowed", sa.Boolean(), nullable=False), - sa.Column("status", sa.String(length=40), nullable=False), - sa.Column("resulting_target_id", sa.String(length=80), nullable=True), - sa.Column("error_message", sa.Text(), nullable=True), - sa.Column("reviewed_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("reviewed_by", sa.String(length=80), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.ForeignKeyConstraint( - ["cycle_id"], - ["big_cycles.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_evolution_candidates_character_id"), - "evolution_candidates", - ["character_id"], - unique=False, - ) - op.create_index( - op.f("ix_evolution_candidates_cycle_id"), "evolution_candidates", ["cycle_id"], unique=False - ) - op.create_index( - op.f("ix_evolution_candidates_status"), "evolution_candidates", ["status"], unique=False - ) - # 资料导入条目 - op.create_table( - "import_items", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("import_job_id", sa.String(length=80), nullable=False), - sa.Column("character_id", sa.String(length=80), nullable=False), - sa.Column("item_type", sa.String(length=40), nullable=False), - sa.Column("role", sa.String(length=40), nullable=False), - sa.Column("sequence", sa.Integer(), nullable=False), - sa.Column("content", sa.Text(), nullable=False), - sa.Column("metadata_json", sa.Text(), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.ForeignKeyConstraint( - ["import_job_id"], - ["character_import_jobs.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_import_items_character_id"), "import_items", ["character_id"], unique=False - ) - op.create_index( - op.f("ix_import_items_import_job_id"), "import_items", ["import_job_id"], unique=False - ) - # 记忆候选 - op.create_table( - "memory_candidates", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("character_id", sa.String(length=80), nullable=False), - sa.Column("import_job_id", sa.String(length=80), nullable=True), - sa.Column("cycle_id", sa.String(length=80), nullable=True), - sa.Column("type", sa.String(length=60), nullable=False), - sa.Column("title", sa.String(length=200), nullable=False), - sa.Column("content", sa.Text(), nullable=False), - sa.Column("confidence", sa.Float(), nullable=False), - sa.Column("importance", sa.Float(), nullable=False), - sa.Column("sensitivity", sa.String(length=40), nullable=False), - sa.Column("evidence_ids_json", sa.Text(), nullable=False), - sa.Column("status", sa.String(length=40), nullable=False), - sa.Column("reason", sa.Text(), nullable=True), - sa.Column("reviewed_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.ForeignKeyConstraint( - ["cycle_id"], - ["big_cycles.id"], - ), - sa.ForeignKeyConstraint( - ["import_job_id"], - ["character_import_jobs.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_memory_candidates_character_id"), - "memory_candidates", - ["character_id"], - unique=False, - ) - op.create_index( - op.f("ix_memory_candidates_cycle_id"), "memory_candidates", ["cycle_id"], unique=False - ) - op.create_index( - op.f("ix_memory_candidates_import_job_id"), - "memory_candidates", - ["import_job_id"], - unique=False, - ) - op.create_index( - op.f("ix_memory_candidates_status"), "memory_candidates", ["status"], unique=False - ) - # 记忆关联 - op.create_table( - "memory_links", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("character_id", sa.String(length=80), nullable=False), - sa.Column("source_memory_id", sa.String(length=80), nullable=False), - sa.Column("target_memory_id", sa.String(length=80), nullable=False), - sa.Column("relation", sa.String(length=80), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.ForeignKeyConstraint( - ["source_memory_id"], - ["memories.id"], - ), - sa.ForeignKeyConstraint( - ["target_memory_id"], - ["memories.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_memory_links_character_id"), "memory_links", ["character_id"], unique=False - ) - op.create_index( - op.f("ix_memory_links_source_memory_id"), "memory_links", ["source_memory_id"], unique=False - ) - op.create_index( - op.f("ix_memory_links_target_memory_id"), "memory_links", ["target_memory_id"], unique=False - ) - # 人格建议 - op.create_table( - "personality_proposals", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("character_id", sa.String(length=80), nullable=False), - sa.Column("fields_json", sa.Text(), nullable=False), - sa.Column("reason", sa.Text(), nullable=False), - sa.Column("status", sa.String(length=40), nullable=False), - sa.Column("resulting_version_id", sa.String(length=80), nullable=True), - sa.Column("reviewed_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("reviewed_by", sa.String(length=80), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.ForeignKeyConstraint( - ["resulting_version_id"], - ["personality_versions.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_personality_proposals_character_id"), - "personality_proposals", - ["character_id"], - unique=False, - ) - op.create_index( - op.f("ix_personality_proposals_status"), "personality_proposals", ["status"], unique=False - ) - # 洛书计划 - op.create_table( - "plans", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("goal_id", sa.String(length=80), nullable=True), - sa.Column("title", sa.String(length=300), nullable=False), - sa.Column("status", sa.String(length=40), nullable=False), - sa.Column("assumptions_json", sa.Text(), nullable=False), - sa.Column("risks_json", sa.Text(), nullable=False), - sa.Column("required_tools_json", sa.Text(), nullable=False), - sa.Column("success_criteria_json", sa.Text(), nullable=False), - sa.Column("step_count", sa.Integer(), nullable=False), - sa.Column("completed_steps", sa.Integer(), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["goal_id"], - ["goals.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index(op.f("ix_plans_goal_id"), "plans", ["goal_id"], unique=False) - op.create_index(op.f("ix_plans_status"), "plans", ["status"], unique=False) - # 运行记录 - op.create_table( - "runs", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("character_id", sa.String(length=80), nullable=False), - sa.Column("session_id", sa.String(length=80), nullable=True), - sa.Column("status", sa.String(length=40), nullable=False), - sa.Column("goal", sa.String(length=300), nullable=True), - sa.Column("origin", sa.String(length=40), nullable=False), - sa.Column("model_hint", sa.String(length=120), nullable=True), - sa.Column("started_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("error_code", sa.String(length=80), nullable=True), - sa.Column("error_message", sa.Text(), nullable=True), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.ForeignKeyConstraint( - ["session_id"], - ["chat_sessions.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index(op.f("ix_runs_character_id"), "runs", ["character_id"], unique=False) - op.create_index(op.f("ix_runs_session_id"), "runs", ["session_id"], unique=False) - op.create_index(op.f("ix_runs_status"), "runs", ["status"], unique=False) - # 世界关系 - op.create_table( - "world_relations", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("character_id", sa.String(length=80), nullable=False), - sa.Column("subject_id", sa.String(length=80), nullable=False), - sa.Column("predicate", sa.String(length=120), nullable=False), - sa.Column("object_id", sa.String(length=80), nullable=False), - sa.Column("confidence", sa.Float(), nullable=False), - sa.Column("evidence_ids_json", sa.Text(), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.ForeignKeyConstraint( - ["object_id"], - ["character_world_entities.id"], - ), - sa.ForeignKeyConstraint( - ["subject_id"], - ["character_world_entities.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_world_relations_character_id"), "world_relations", ["character_id"], unique=False - ) - op.create_index( - op.f("ix_world_relations_object_id"), "world_relations", ["object_id"], unique=False - ) - op.create_index( - op.f("ix_world_relations_subject_id"), "world_relations", ["subject_id"], unique=False - ) - # 起居注审计事件 - op.create_table( - "audit_events", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("pivot", sa.String(length=40), nullable=True), - sa.Column("character_id", sa.String(length=80), nullable=True), - sa.Column("run_id", sa.String(length=80), nullable=True), - sa.Column("action", sa.String(length=120), nullable=False), - sa.Column("actor", sa.String(length=80), nullable=False), - sa.Column("risk_level", sa.String(length=20), nullable=False), - sa.Column("decision", sa.String(length=40), nullable=False), - sa.Column("reason", sa.Text(), nullable=True), - sa.Column("input_summary", sa.Text(), nullable=True), - sa.Column("output_summary", sa.Text(), nullable=True), - sa.Column("trace_id", sa.String(length=80), nullable=True), - sa.Column("resource_type", sa.String(length=80), nullable=True), - sa.Column("resource_id", sa.String(length=120), nullable=True), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.ForeignKeyConstraint( - ["run_id"], - ["runs.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_audit_events_character_id"), "audit_events", ["character_id"], unique=False - ) - op.create_index(op.f("ix_audit_events_pivot"), "audit_events", ["pivot"], unique=False) - op.create_index(op.f("ix_audit_events_run_id"), "audit_events", ["run_id"], unique=False) - op.create_index(op.f("ix_audit_events_trace_id"), "audit_events", ["trace_id"], unique=False) - # 对话消息 - op.create_table( - "chat_messages", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("character_id", sa.String(length=80), nullable=False), - sa.Column("session_id", sa.String(length=80), nullable=False), - sa.Column("role", sa.String(length=40), nullable=False), - sa.Column("content", sa.Text(), nullable=False), - sa.Column("modality", sa.String(length=40), nullable=False), - sa.Column("run_id", sa.String(length=80), nullable=True), - sa.Column("metadata_json", sa.Text(), nullable=False), - sa.Column("sensitivity", sa.String(length=40), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.ForeignKeyConstraint( - ["run_id"], - ["runs.id"], - ), - sa.ForeignKeyConstraint( - ["session_id"], - ["chat_sessions.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_chat_messages_character_id"), "chat_messages", ["character_id"], unique=False - ) - op.create_index(op.f("ix_chat_messages_run_id"), "chat_messages", ["run_id"], unique=False) - op.create_index( - op.f("ix_chat_messages_session_id"), "chat_messages", ["session_id"], unique=False - ) - # 一炁事件 - op.create_table( - "events", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("character_id", sa.String(length=80), nullable=True), - sa.Column("run_id", sa.String(length=80), nullable=True), - sa.Column("type", sa.String(length=80), nullable=False), - sa.Column("source", sa.String(length=40), nullable=False), - sa.Column("payload_json", sa.Text(), nullable=False), - sa.Column("visibility", sa.String(length=40), nullable=False), - sa.Column("sensitivity", sa.String(length=40), nullable=False), - sa.Column("trace_id", sa.String(length=80), nullable=True), - sa.Column("parent_id", sa.String(length=80), nullable=True), - sa.Column("sequence", sa.Integer(), nullable=False), - sa.Column("evidence_ids_json", sa.Text(), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.ForeignKeyConstraint( - ["run_id"], - ["runs.id"], - ), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("run_id", "sequence", name="uq_events_run_sequence"), - ) - op.create_index(op.f("ix_events_character_id"), "events", ["character_id"], unique=False) - op.create_index(op.f("ix_events_run_id"), "events", ["run_id"], unique=False) - op.create_index(op.f("ix_events_trace_id"), "events", ["trace_id"], unique=False) - op.create_index(op.f("ix_events_type"), "events", ["type"], unique=False) - # 探索性次生目标 - op.create_table( - "exploratory_subgoals", - sa.Column("id", sa.String(length=80), nullable=False), - sa.Column("character_id", sa.String(length=80), nullable=False), - sa.Column("cycle_id", sa.String(length=80), nullable=False), - sa.Column("parent_goal_id", sa.String(length=80), nullable=True), - sa.Column("curiosity_question_id", sa.String(length=80), nullable=False), - sa.Column("title", sa.String(length=200), nullable=False), - sa.Column("description", sa.Text(), nullable=False), - sa.Column("expected_information_gain", sa.Float(), nullable=False), - sa.Column("interruption_cost", sa.Float(), nullable=False), - sa.Column("risk_level", sa.String(length=8), nullable=False), - sa.Column("status", sa.String(length=40), nullable=False), - sa.Column("reviewed_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("reviewed_by", sa.String(length=80), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["character_id"], - ["characters.id"], - ), - sa.ForeignKeyConstraint( - ["curiosity_question_id"], - ["curiosity_questions.id"], - ), - sa.ForeignKeyConstraint( - ["cycle_id"], - ["big_cycles.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_exploratory_subgoals_character_id"), - "exploratory_subgoals", - ["character_id"], - unique=False, - ) - op.create_index( - op.f("ix_exploratory_subgoals_curiosity_question_id"), - "exploratory_subgoals", - ["curiosity_question_id"], - unique=False, - ) - op.create_index( - op.f("ix_exploratory_subgoals_cycle_id"), "exploratory_subgoals", ["cycle_id"], unique=False - ) - op.create_index( - op.f("ix_exploratory_subgoals_status"), "exploratory_subgoals", ["status"], unique=False - ) - # RBAC 角色 - op.create_table( - "roles", - sa.Column("id", sa.String(40), nullable=False), - sa.Column("name", sa.String(80), nullable=False), - sa.Column("description", sa.Text(), nullable=False, server_default=""), - sa.Column("is_system", sa.Boolean(), nullable=False, server_default=sa.text("false")), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index("ix_roles_name", "roles", ["name"], unique=True) - - # RBAC 权限 - op.create_table( - "permissions", - sa.Column("id", sa.String(40), nullable=False), - sa.Column("resource", sa.String(80), nullable=False), - sa.Column("action", sa.String(80), nullable=False), - sa.Column("description", sa.Text(), nullable=False, server_default=""), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index("ix_permissions_resource", "permissions", ["resource"]) - - # RBAC 用户-角色关联 - op.create_table( - "user_roles", - sa.Column("user_id", sa.String(40), nullable=False), - sa.Column("role_id", sa.String(40), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint("user_id", "role_id"), - sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint(["role_id"], ["roles.id"], ondelete="CASCADE"), - ) - - # RBAC 角色-权限关联 - op.create_table( - "role_permissions", - sa.Column("role_id", sa.String(40), nullable=False), - sa.Column("permission_id", sa.String(40), nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint("role_id", "permission_id"), - sa.ForeignKeyConstraint(["role_id"], ["roles.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint(["permission_id"], ["permissions.id"], ondelete="CASCADE"), - ) - # 消息版本历史 - op.add_column( - "chat_messages", - sa.Column( - "versions_json", - sa.Text, - nullable=False, - server_default="[]", - comment="版本历史 JSON 数组:[{content, created_at}],按生成顺序排列,末尾最新", - ), - ) - op.add_column( - "chat_messages", - sa.Column( - "active_version_idx", - sa.Integer, - nullable=False, - server_default=sa.text("0"), - comment="当前活跃版本在 versions_json 中的索引(0-based)", - ), - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - op.drop_column("chat_messages", "active_version_idx") - op.drop_column("chat_messages", "versions_json") - op.drop_table("role_permissions") - op.drop_table("user_roles") - op.drop_index("ix_permissions_resource", table_name="permissions") - op.drop_table("permissions") - op.drop_index("ix_roles_name", table_name="roles") - op.drop_table("roles") - op.drop_index(op.f("ix_exploratory_subgoals_status"), table_name="exploratory_subgoals") - op.drop_index(op.f("ix_exploratory_subgoals_cycle_id"), table_name="exploratory_subgoals") - op.drop_index( - op.f("ix_exploratory_subgoals_curiosity_question_id"), table_name="exploratory_subgoals" - ) - op.drop_index(op.f("ix_exploratory_subgoals_character_id"), table_name="exploratory_subgoals") - op.drop_table("exploratory_subgoals") - op.drop_index(op.f("ix_events_type"), table_name="events") - op.drop_index(op.f("ix_events_trace_id"), table_name="events") - op.drop_index(op.f("ix_events_run_id"), table_name="events") - op.drop_index(op.f("ix_events_character_id"), table_name="events") - op.drop_table("events") - op.drop_index(op.f("ix_chat_messages_session_id"), table_name="chat_messages") - op.drop_index(op.f("ix_chat_messages_run_id"), table_name="chat_messages") - op.drop_index(op.f("ix_chat_messages_character_id"), table_name="chat_messages") - op.drop_table("chat_messages") - op.drop_index(op.f("ix_audit_events_trace_id"), table_name="audit_events") - op.drop_index(op.f("ix_audit_events_run_id"), table_name="audit_events") - op.drop_index(op.f("ix_audit_events_pivot"), table_name="audit_events") - op.drop_index(op.f("ix_audit_events_character_id"), table_name="audit_events") - op.drop_table("audit_events") - op.drop_index(op.f("ix_world_relations_subject_id"), table_name="world_relations") - op.drop_index(op.f("ix_world_relations_object_id"), table_name="world_relations") - op.drop_index(op.f("ix_world_relations_character_id"), table_name="world_relations") - op.drop_table("world_relations") - op.drop_index(op.f("ix_runs_status"), table_name="runs") - op.drop_index(op.f("ix_runs_session_id"), table_name="runs") - op.drop_index(op.f("ix_runs_character_id"), table_name="runs") - op.drop_table("runs") - op.drop_index(op.f("ix_plans_status"), table_name="plans") - op.drop_index(op.f("ix_plans_goal_id"), table_name="plans") - op.drop_table("plans") - op.drop_index(op.f("ix_personality_proposals_status"), table_name="personality_proposals") - op.drop_index(op.f("ix_personality_proposals_character_id"), table_name="personality_proposals") - op.drop_table("personality_proposals") - op.drop_index(op.f("ix_memory_links_target_memory_id"), table_name="memory_links") - op.drop_index(op.f("ix_memory_links_source_memory_id"), table_name="memory_links") - op.drop_index(op.f("ix_memory_links_character_id"), table_name="memory_links") - op.drop_table("memory_links") - op.drop_index(op.f("ix_memory_candidates_status"), table_name="memory_candidates") - op.drop_index(op.f("ix_memory_candidates_import_job_id"), table_name="memory_candidates") - op.drop_index(op.f("ix_memory_candidates_cycle_id"), table_name="memory_candidates") - op.drop_index(op.f("ix_memory_candidates_character_id"), table_name="memory_candidates") - op.drop_table("memory_candidates") - op.drop_index(op.f("ix_import_items_import_job_id"), table_name="import_items") - op.drop_index(op.f("ix_import_items_character_id"), table_name="import_items") - op.drop_table("import_items") - op.drop_index(op.f("ix_evolution_candidates_status"), table_name="evolution_candidates") - op.drop_index(op.f("ix_evolution_candidates_cycle_id"), table_name="evolution_candidates") - op.drop_index(op.f("ix_evolution_candidates_character_id"), table_name="evolution_candidates") - op.drop_table("evolution_candidates") - op.drop_index(op.f("ix_curiosity_questions_cycle_id"), table_name="curiosity_questions") - op.drop_index(op.f("ix_curiosity_questions_character_id"), table_name="curiosity_questions") - op.drop_table("curiosity_questions") - op.drop_index( - op.f("ix_character_profile_candidates_status"), table_name="character_profile_candidates" - ) - op.drop_index( - op.f("ix_character_profile_candidates_import_job_id"), - table_name="character_profile_candidates", - ) - op.drop_index( - op.f("ix_character_profile_candidates_character_id"), - table_name="character_profile_candidates", - ) - op.drop_table("character_profile_candidates") - op.drop_index(op.f("ix_time_anniversaries_character_id"), table_name="time_anniversaries") - op.drop_table("time_anniversaries") - op.drop_index(op.f("ix_personality_versions_character_id"), table_name="personality_versions") - op.drop_table("personality_versions") - op.drop_index(op.f("ix_memories_status"), table_name="memories") - op.drop_index(op.f("ix_memories_character_id"), table_name="memories") - op.drop_table("memories") - op.drop_index(op.f("ix_goals_status"), table_name="goals") - op.drop_index(op.f("ix_goals_level"), table_name="goals") - op.drop_index(op.f("ix_goals_character_id"), table_name="goals") - op.drop_table("goals") - op.drop_index(op.f("ix_chat_sessions_status"), table_name="chat_sessions") - op.drop_index(op.f("ix_chat_sessions_character_id"), table_name="chat_sessions") - op.drop_table("chat_sessions") - op.drop_index(op.f("ix_character_world_entities_type"), table_name="character_world_entities") - op.drop_index(op.f("ix_character_world_entities_status"), table_name="character_world_entities") - op.drop_index( - op.f("ix_character_world_entities_character_id"), table_name="character_world_entities" - ) - op.drop_table("character_world_entities") - op.drop_index(op.f("ix_character_import_jobs_status"), table_name="character_import_jobs") - op.drop_index( - op.f("ix_character_import_jobs_material_type"), table_name="character_import_jobs" - ) - op.drop_index(op.f("ix_character_import_jobs_checksum"), table_name="character_import_jobs") - op.drop_index(op.f("ix_character_import_jobs_character_id"), table_name="character_import_jobs") - op.drop_table("character_import_jobs") - op.drop_table("character_emotion_states") - op.drop_index(op.f("ix_big_cycles_status"), table_name="big_cycles") - op.drop_index(op.f("ix_big_cycles_completed_at"), table_name="big_cycles") - op.drop_index(op.f("ix_big_cycles_character_id"), table_name="big_cycles") - op.drop_table("big_cycles") - op.drop_index(op.f("ix_world_entities_type"), table_name="world_entities") - op.drop_table("world_entities") - op.drop_index(op.f("ix_users_username"), table_name="users") - op.drop_table("users") - op.drop_table("tool_policies") - op.drop_index(op.f("ix_skills_status"), table_name="skills") - op.drop_table("skills") - op.drop_table("perception_channels") - op.drop_index(op.f("ix_pending_confirmations_status"), table_name="pending_confirmations") - op.drop_index(op.f("ix_pending_confirmations_run_id"), table_name="pending_confirmations") - op.drop_index(op.f("ix_pending_confirmations_character_id"), table_name="pending_confirmations") - op.drop_table("pending_confirmations") - op.drop_table("model_endpoint_overrides") - op.drop_index(op.f("ix_mcp_servers_status"), table_name="mcp_servers") - op.drop_table("mcp_servers") - op.drop_index(op.f("ix_idempotency_keys_trace_id"), table_name="idempotency_keys") - op.drop_index(op.f("ix_idempotency_keys_scope"), table_name="idempotency_keys") - op.drop_index(op.f("ix_idempotency_keys_key"), table_name="idempotency_keys") - op.drop_index(op.f("ix_idempotency_keys_expires_at"), table_name="idempotency_keys") - op.drop_table("idempotency_keys") - op.drop_table("generation_preferences") - op.drop_index(op.f("ix_characters_template_key"), table_name="characters") - op.drop_index(op.f("ix_characters_status"), table_name="characters") - op.drop_index(op.f("ix_characters_source_template_id"), table_name="characters") - op.drop_index(op.f("ix_characters_owner_scope"), table_name="characters") - op.drop_index(op.f("ix_characters_is_system_template"), table_name="characters") - op.drop_table("characters") - op.drop_table("agent_roster") - # ### end Alembic commands ### diff --git a/backend/alembic/versions/002_add_chat_session_owner_scope.py b/backend/alembic/versions/002_add_chat_session_owner_scope.py deleted file mode 100644 index b9ac7df..0000000 --- a/backend/alembic/versions/002_add_chat_session_owner_scope.py +++ /dev/null @@ -1,56 +0,0 @@ -"""add_chat_session_owner_scope - -Revision ID: 002 -Revises: 001 -Create Date: 2026-07-14 07:54:32.969191 - -""" -from collections.abc import Sequence - -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = '002' -down_revision: str | Sequence[str] | None = '001' -branch_labels: str | Sequence[str] | None = None -depends_on: str | Sequence[str] | None = None - - -def upgrade() -> None: - conn = op.get_bind() - is_sqlite = conn.engine.dialect.name == "sqlite" - - if is_sqlite: - # SQLite:用 batch 模式加列(新增 NOT NULL 列需 batch recreating) - with op.batch_alter_table("chat_sessions") as batch_op: - batch_op.add_column( - sa.Column("owner_scope", sa.String(length=80), nullable=False, server_default=""), - ) - batch_op.create_index("ix_chat_sessions_owner_scope", ["owner_scope"]) - # 移除 server_default(仅 DDL 阶段需要) - with op.batch_alter_table("chat_sessions") as batch_op: - batch_op.alter_column("owner_scope", server_default=None) - else: - op.add_column( - "chat_sessions", - sa.Column("owner_scope", sa.String(length=80), nullable=False, server_default=""), - ) - op.create_index( - op.f("ix_chat_sessions_owner_scope"), "chat_sessions", ["owner_scope"], unique=False - ) - op.alter_column("chat_sessions", "owner_scope", server_default=None) - - -def downgrade() -> None: - conn = op.get_bind() - is_sqlite = conn.engine.dialect.name == "sqlite" - - if is_sqlite: - with op.batch_alter_table("chat_sessions") as batch_op: - batch_op.drop_index("ix_chat_sessions_owner_scope") - batch_op.drop_column("owner_scope") - else: - op.drop_index(op.f("ix_chat_sessions_owner_scope"), table_name="chat_sessions") - op.drop_column("chat_sessions", "owner_scope") diff --git a/backend/alembic/versions/003_add_daoti_soul_fields_to_character.py b/backend/alembic/versions/003_add_daoti_soul_fields_to_character.py deleted file mode 100644 index b708c7e..0000000 --- a/backend/alembic/versions/003_add_daoti_soul_fields_to_character.py +++ /dev/null @@ -1,36 +0,0 @@ -"""add daoti soul fields to character - -Revision ID: 003 -Revises: 002 -Create Date: 2026-07-24 10:47:40.993422 - -""" -from collections.abc import Sequence - -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = '003' -down_revision: str | Sequence[str] | None = '002' -branch_labels: str | Sequence[str] | None = None -depends_on: str | Sequence[str] | None = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('characters', sa.Column('personality_summary', sa.Text(), nullable=True)) - op.add_column('characters', sa.Column('mission', sa.Text(), nullable=True)) - op.add_column('characters', sa.Column('safety_guidelines', sa.Text(), nullable=True)) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('characters', 'safety_guidelines') - op.drop_column('characters', 'mission') - op.drop_column('characters', 'personality_summary') - # ### end Alembic commands ### diff --git a/backend/alembic/versions/004_add_lorebook_tables.py b/backend/alembic/versions/004_add_lorebook_tables.py deleted file mode 100644 index cd346ba..0000000 --- a/backend/alembic/versions/004_add_lorebook_tables.py +++ /dev/null @@ -1,81 +0,0 @@ -"""add lorebook tables - -Revision ID: 004 -Revises: 003 -Create Date: 2026-07-24 12:20:38.453954 - -""" -from collections.abc import Sequence - -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = '004' -down_revision: str | Sequence[str] | None = '003' -branch_labels: str | Sequence[str] | None = None -depends_on: str | Sequence[str] | None = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('lorebooks', - sa.Column('id', sa.String(length=80), nullable=False), - sa.Column('character_id', sa.String(length=80), nullable=True), - sa.Column('name', sa.String(length=200), nullable=False), - sa.Column('description', sa.Text(), nullable=False), - sa.Column('scan_depth', sa.Integer(), nullable=True), - sa.Column('token_budget', sa.Integer(), nullable=True), - sa.Column('recursive_scanning', sa.Boolean(), nullable=False), - sa.Column('scope', sa.String(length=40), nullable=False), - sa.Column('status', sa.String(length=40), nullable=False), - sa.Column('owner_scope', sa.String(length=80), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_lorebooks_character_id'), 'lorebooks', ['character_id'], unique=False) - op.create_index(op.f('ix_lorebooks_owner_scope'), 'lorebooks', ['owner_scope'], unique=False) - op.create_index(op.f('ix_lorebooks_scope'), 'lorebooks', ['scope'], unique=False) - op.create_index(op.f('ix_lorebooks_status'), 'lorebooks', ['status'], unique=False) - op.create_table('lorebook_entries', - sa.Column('id', sa.String(length=80), nullable=False), - sa.Column('lorebook_id', sa.String(length=80), nullable=False), - sa.Column('keys_json', sa.Text(), nullable=False), - sa.Column('content', sa.Text(), nullable=False), - sa.Column('secondary_keys_json', sa.Text(), nullable=False), - sa.Column('constant', sa.Boolean(), nullable=False), - sa.Column('selective', sa.Boolean(), nullable=False), - sa.Column('enabled', sa.Boolean(), nullable=False), - sa.Column('case_sensitive', sa.Boolean(), nullable=False), - sa.Column('use_regex', sa.Boolean(), nullable=False), - sa.Column('position', sa.String(length=40), nullable=False), - sa.Column('insertion_order', sa.Integer(), nullable=False), - sa.Column('priority', sa.Integer(), nullable=False), - sa.Column('comment', sa.String(length=500), nullable=False), - sa.Column('bound_entity_id', sa.String(length=80), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint(['lorebook_id'], ['lorebooks.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_lorebook_entries_bound_entity_id'), 'lorebook_entries', ['bound_entity_id'], unique=False) - op.create_index(op.f('ix_lorebook_entries_lorebook_id'), 'lorebook_entries', ['lorebook_id'], unique=False) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_lorebook_entries_lorebook_id'), table_name='lorebook_entries') - op.drop_index(op.f('ix_lorebook_entries_bound_entity_id'), table_name='lorebook_entries') - op.drop_table('lorebook_entries') - op.drop_index(op.f('ix_lorebooks_status'), table_name='lorebooks') - op.drop_index(op.f('ix_lorebooks_scope'), table_name='lorebooks') - op.drop_index(op.f('ix_lorebooks_owner_scope'), table_name='lorebooks') - op.drop_index(op.f('ix_lorebooks_character_id'), table_name='lorebooks') - op.drop_table('lorebooks') - # ### end Alembic commands ### diff --git a/backend/alembic/versions/843abf719fde_rewrite_1_0_基线_合并既有模式为单一起点.py b/backend/alembic/versions/843abf719fde_rewrite_1_0_基线_合并既有模式为单一起点.py new file mode 100644 index 0000000..01850e5 --- /dev/null +++ b/backend/alembic/versions/843abf719fde_rewrite_1_0_基线_合并既有模式为单一起点.py @@ -0,0 +1,889 @@ +"""rewrite-1.0 基线:合并既有模式为单一起点 + +Revision ID: 843abf719fde +Revises: +Create Date: 2026-08-21 21:57:55.890597 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '843abf719fde' +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('agent_roster', + sa.Column('role', sa.String(length=80), nullable=False), + sa.Column('name', sa.String(length=120), nullable=False), + sa.Column('responsibility', sa.Text(), nullable=False), + sa.Column('enabled', sa.Boolean(), nullable=False), + sa.Column('last_active_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint('role') + ) + op.create_table('characters', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('name', sa.String(length=120), nullable=False), + sa.Column('display_name', sa.String(length=120), nullable=True), + sa.Column('status', sa.String(length=40), nullable=False), + sa.Column('language', sa.String(length=20), nullable=False), + sa.Column('profile_json', sa.Text(), nullable=False), + sa.Column('model_policy_json', sa.Text(), nullable=False), + sa.Column('safety_policy_id', sa.String(length=80), nullable=True), + sa.Column('current_personality_version_id', sa.String(length=80), nullable=True), + sa.Column('personality_summary', sa.Text(), nullable=True), + sa.Column('mission', sa.Text(), nullable=True), + sa.Column('safety_guidelines', sa.Text(), nullable=True), + sa.Column('current_mood_id', sa.String(length=80), nullable=True), + sa.Column('privacy_level', sa.String(length=40), nullable=False), + sa.Column('owner_scope', sa.String(length=80), nullable=False), + sa.Column('is_system_template', sa.Boolean(), nullable=False), + sa.Column('source_template_id', sa.String(length=80), nullable=True), + sa.Column('template_key', sa.String(length=80), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('activated_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('archived_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_characters_is_system_template'), 'characters', ['is_system_template'], unique=False) + op.create_index(op.f('ix_characters_owner_scope'), 'characters', ['owner_scope'], unique=False) + op.create_index(op.f('ix_characters_source_template_id'), 'characters', ['source_template_id'], unique=False) + op.create_index(op.f('ix_characters_status'), 'characters', ['status'], unique=False) + op.create_index(op.f('ix_characters_template_key'), 'characters', ['template_key'], unique=False) + op.create_table('generation_preferences', + sa.Column('owner_scope', sa.String(length=80), nullable=False), + sa.Column('default_model_hint', sa.String(length=120), nullable=False), + sa.Column('temperature', sa.Float(), nullable=False), + sa.Column('top_p', sa.Float(), nullable=False), + sa.Column('max_output_tokens', sa.Integer(), nullable=False), + sa.Column('streaming', sa.Boolean(), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint('owner_scope') + ) + op.create_table('idempotency_keys', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('key', sa.String(length=200), nullable=False), + sa.Column('scope', sa.String(length=120), nullable=False), + sa.Column('method', sa.String(length=20), nullable=False), + sa.Column('path', sa.String(length=500), nullable=False), + sa.Column('request_hash', sa.String(length=128), nullable=False), + sa.Column('status', sa.String(length=40), nullable=False), + sa.Column('response_status_code', sa.Integer(), nullable=False), + sa.Column('response_body_json', sa.Text(), nullable=False), + sa.Column('resource_type', sa.String(length=80), nullable=True), + sa.Column('resource_id', sa.String(length=120), nullable=True), + sa.Column('trace_id', sa.String(length=80), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('scope', 'key', name='uq_idempotency_scope_key') + ) + op.create_index(op.f('ix_idempotency_keys_expires_at'), 'idempotency_keys', ['expires_at'], unique=False) + op.create_index(op.f('ix_idempotency_keys_key'), 'idempotency_keys', ['key'], unique=False) + op.create_index(op.f('ix_idempotency_keys_scope'), 'idempotency_keys', ['scope'], unique=False) + op.create_index(op.f('ix_idempotency_keys_trace_id'), 'idempotency_keys', ['trace_id'], unique=False) + op.create_table('mcp_servers', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('name', sa.String(length=120), nullable=False), + sa.Column('transport', sa.String(length=40), nullable=False), + sa.Column('endpoint_url', sa.String(length=500), nullable=True), + sa.Column('auth_token', sa.String(length=500), nullable=True), + sa.Column('command', sa.String(length=500), nullable=True), + sa.Column('args_json', sa.Text(), nullable=True), + sa.Column('env_json', sa.Text(), nullable=True), + sa.Column('cwd', sa.String(length=500), nullable=True), + sa.Column('status', sa.String(length=40), nullable=False), + sa.Column('tools_json', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_index(op.f('ix_mcp_servers_status'), 'mcp_servers', ['status'], unique=False) + op.create_table('model_endpoint_overrides', + sa.Column('logical_model_name', sa.String(length=120), nullable=False), + sa.Column('endpoint_name', sa.String(length=120), nullable=False), + sa.Column('base_url', sa.String(length=500), nullable=True), + sa.Column('api_key', sa.String(length=500), nullable=True), + sa.Column('status', sa.String(length=40), nullable=True), + sa.Column('weight', sa.Integer(), nullable=True), + sa.Column('priority', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint('logical_model_name', 'endpoint_name') + ) + op.create_table('pending_confirmations', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('tool', sa.String(length=120), nullable=False), + sa.Column('input_json', sa.Text(), nullable=False), + sa.Column('risk_level', sa.String(length=40), nullable=False), + sa.Column('reason', sa.Text(), nullable=False), + sa.Column('run_id', sa.String(length=80), nullable=True), + sa.Column('character_id', sa.String(length=80), nullable=True), + sa.Column('trace_id', sa.String(length=80), nullable=True), + sa.Column('status', sa.String(length=40), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('resolved_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_pending_confirmations_character_id'), 'pending_confirmations', ['character_id'], unique=False) + op.create_index(op.f('ix_pending_confirmations_run_id'), 'pending_confirmations', ['run_id'], unique=False) + op.create_index(op.f('ix_pending_confirmations_status'), 'pending_confirmations', ['status'], unique=False) + op.create_table('perception_channels', + sa.Column('channel', sa.String(length=80), nullable=False), + sa.Column('name', sa.String(length=120), nullable=False), + sa.Column('description', sa.Text(), nullable=False), + sa.Column('enabled', sa.Boolean(), nullable=False), + sa.Column('consent_status', sa.String(length=40), nullable=False), + sa.Column('privacy_level', sa.String(length=40), nullable=False), + sa.Column('risk_level', sa.String(length=20), nullable=False), + sa.Column('last_active_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint('channel') + ) + op.create_table('permissions', + sa.Column('id', sa.String(length=40), nullable=False), + sa.Column('resource', sa.String(length=80), nullable=False), + sa.Column('action', sa.String(length=80), nullable=False), + sa.Column('description', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_permissions_resource'), 'permissions', ['resource'], unique=False) + op.create_table('roles', + sa.Column('id', sa.String(length=40), nullable=False), + sa.Column('name', sa.String(length=80), nullable=False), + sa.Column('description', sa.Text(), nullable=False), + sa.Column('is_system', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_roles_name'), 'roles', ['name'], unique=True) + op.create_table('skills', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('name', sa.String(length=120), nullable=False), + sa.Column('description', sa.Text(), nullable=False), + sa.Column('trigger', sa.Text(), nullable=False), + sa.Column('prompt_fragment', sa.Text(), nullable=False), + sa.Column('tool_bindings_json', sa.Text(), nullable=False), + sa.Column('risk_level', sa.String(length=8), nullable=False), + sa.Column('status', sa.String(length=40), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_index(op.f('ix_skills_status'), 'skills', ['status'], unique=False) + op.create_table('tool_policies', + sa.Column('name', sa.String(length=80), nullable=False), + sa.Column('description', sa.Text(), nullable=False), + sa.Column('risk_level', sa.String(length=20), nullable=False), + sa.Column('permission_policy', sa.String(length=20), nullable=False), + sa.Column('side_effect', sa.Boolean(), nullable=False), + sa.Column('audit_required', sa.Boolean(), nullable=False), + sa.Column('source', sa.String(length=20), nullable=False), + sa.Column('enabled', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint('name') + ) + op.create_table('users', + sa.Column('id', sa.String(length=40), nullable=False), + sa.Column('username', sa.String(length=80), nullable=False), + sa.Column('email', sa.String(length=255), nullable=False), + sa.Column('password_hash', sa.String(length=255), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('is_admin', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email') + ) + op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True) + op.create_table('world_entities', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('type', sa.String(length=40), nullable=False), + sa.Column('name', sa.String(length=200), nullable=False), + sa.Column('attributes_json', sa.Text(), nullable=False), + sa.Column('state_type', sa.String(length=40), nullable=False), + sa.Column('confidence', sa.Float(), nullable=False), + sa.Column('sources_json', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_world_entities_type'), 'world_entities', ['type'], unique=False) + op.create_table('big_cycles', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('character_id', sa.String(length=80), nullable=False), + sa.Column('trigger', sa.String(length=40), nullable=False), + sa.Column('status', sa.String(length=40), nullable=False), + sa.Column('phase', sa.String(length=40), nullable=True), + sa.Column('memory_count_at_trigger', sa.Integer(), nullable=False), + sa.Column('evaluation_snapshot_json', sa.Text(), nullable=True), + sa.Column('summary', sa.Text(), nullable=True), + sa.Column('error_code', sa.String(length=80), nullable=True), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('started_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_big_cycles_character_id'), 'big_cycles', ['character_id'], unique=False) + op.create_index(op.f('ix_big_cycles_completed_at'), 'big_cycles', ['completed_at'], unique=False) + op.create_index(op.f('ix_big_cycles_status'), 'big_cycles', ['status'], unique=False) + op.create_table('character_emotion_states', + sa.Column('character_id', sa.String(length=80), nullable=False), + sa.Column('mood', sa.String(length=40), nullable=False), + sa.Column('intensity', sa.Float(), nullable=False), + sa.Column('baseline_mood', sa.String(length=40), nullable=False), + sa.Column('baseline_intensity', sa.Float(), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('last_trigger_event_id', sa.String(length=80), nullable=True), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.PrimaryKeyConstraint('character_id') + ) + op.create_table('character_import_jobs', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('character_id', sa.String(length=80), nullable=False), + sa.Column('material_type', sa.String(length=40), nullable=False), + sa.Column('source_type', sa.String(length=40), nullable=False), + sa.Column('title', sa.String(length=200), nullable=True), + sa.Column('status', sa.String(length=40), nullable=False), + sa.Column('format', sa.String(length=40), nullable=False), + sa.Column('media_type', sa.String(length=40), nullable=True), + sa.Column('mime_type', sa.String(length=80), nullable=True), + sa.Column('source_description', sa.Text(), nullable=True), + sa.Column('speaker_label', sa.String(length=120), nullable=True), + sa.Column('consent_status', sa.String(length=40), nullable=False), + sa.Column('usage_scope', sa.String(length=40), nullable=False), + sa.Column('checksum', sa.String(length=120), nullable=True), + sa.Column('item_count', sa.Integer(), nullable=False), + sa.Column('storage_path', sa.String(length=500), nullable=True), + sa.Column('raw_content', sa.Text(), nullable=True), + sa.Column('error_code', sa.String(length=80), nullable=True), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_character_import_jobs_character_id'), 'character_import_jobs', ['character_id'], unique=False) + op.create_index(op.f('ix_character_import_jobs_checksum'), 'character_import_jobs', ['checksum'], unique=False) + op.create_index(op.f('ix_character_import_jobs_material_type'), 'character_import_jobs', ['material_type'], unique=False) + op.create_index(op.f('ix_character_import_jobs_status'), 'character_import_jobs', ['status'], unique=False) + op.create_table('character_world_entities', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('character_id', sa.String(length=80), nullable=False), + sa.Column('type', sa.String(length=40), nullable=False), + sa.Column('name', sa.String(length=200), nullable=False), + sa.Column('attributes_json', sa.Text(), nullable=False), + sa.Column('state_type', sa.String(length=40), nullable=False), + sa.Column('status', sa.String(length=40), nullable=False), + sa.Column('confidence', sa.Float(), nullable=False), + sa.Column('evidence_ids_json', sa.Text(), nullable=False), + sa.Column('sources_json', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_character_world_entities_character_id'), 'character_world_entities', ['character_id'], unique=False) + op.create_index(op.f('ix_character_world_entities_status'), 'character_world_entities', ['status'], unique=False) + op.create_index(op.f('ix_character_world_entities_type'), 'character_world_entities', ['type'], unique=False) + op.create_table('chat_sessions', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('character_id', sa.String(length=80), nullable=False), + sa.Column('title', sa.String(length=200), nullable=True), + sa.Column('status', sa.String(length=40), nullable=False), + sa.Column('owner_scope', sa.String(length=80), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_chat_sessions_character_id'), 'chat_sessions', ['character_id'], unique=False) + op.create_index(op.f('ix_chat_sessions_owner_scope'), 'chat_sessions', ['owner_scope'], unique=False) + op.create_index(op.f('ix_chat_sessions_status'), 'chat_sessions', ['status'], unique=False) + op.create_table('goals', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('character_id', sa.String(length=80), nullable=True), + sa.Column('title', sa.String(length=300), nullable=False), + sa.Column('description', sa.Text(), nullable=False), + sa.Column('level', sa.String(length=40), nullable=False), + sa.Column('type', sa.String(length=40), nullable=False), + sa.Column('status', sa.String(length=40), nullable=False), + sa.Column('priority', sa.Integer(), nullable=False), + sa.Column('progress', sa.Float(), nullable=False), + sa.Column('constraints_json', sa.Text(), nullable=False), + sa.Column('parent_id', sa.String(length=80), nullable=True), + sa.Column('success_criteria_json', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.ForeignKeyConstraint(['parent_id'], ['goals.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_goals_character_id'), 'goals', ['character_id'], unique=False) + op.create_index(op.f('ix_goals_level'), 'goals', ['level'], unique=False) + op.create_index(op.f('ix_goals_status'), 'goals', ['status'], unique=False) + op.create_table('lorebooks', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('character_id', sa.String(length=80), nullable=True), + sa.Column('name', sa.String(length=200), nullable=False), + sa.Column('description', sa.Text(), nullable=False), + sa.Column('scan_depth', sa.Integer(), nullable=True), + sa.Column('token_budget', sa.Integer(), nullable=True), + sa.Column('recursive_scanning', sa.Boolean(), nullable=False), + sa.Column('scope', sa.String(length=40), nullable=False), + sa.Column('status', sa.String(length=40), nullable=False), + sa.Column('owner_scope', sa.String(length=80), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_lorebooks_character_id'), 'lorebooks', ['character_id'], unique=False) + op.create_index(op.f('ix_lorebooks_owner_scope'), 'lorebooks', ['owner_scope'], unique=False) + op.create_index(op.f('ix_lorebooks_scope'), 'lorebooks', ['scope'], unique=False) + op.create_index(op.f('ix_lorebooks_status'), 'lorebooks', ['status'], unique=False) + op.create_table('memories', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('character_id', sa.String(length=80), nullable=False), + sa.Column('type', sa.String(length=60), nullable=False), + sa.Column('title', sa.String(length=200), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('source', sa.String(length=80), nullable=False), + sa.Column('confidence', sa.Float(), nullable=False), + sa.Column('importance', sa.Float(), nullable=False), + sa.Column('sensitivity', sa.String(length=40), nullable=False), + sa.Column('evidence_ids_json', sa.Text(), nullable=False), + sa.Column('status', sa.String(length=40), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('temporal_context_json', sa.Text(), server_default='{}', nullable=False), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_memories_character_id'), 'memories', ['character_id'], unique=False) + op.create_index(op.f('ix_memories_status'), 'memories', ['status'], unique=False) + op.create_table('personality_versions', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('character_id', sa.String(length=80), nullable=False), + sa.Column('version', sa.Integer(), nullable=False), + sa.Column('identity_profile', sa.Text(), nullable=False), + sa.Column('role_destiny', sa.Text(), nullable=False), + sa.Column('personality_seed', sa.Text(), nullable=False), + sa.Column('traits', sa.Text(), nullable=False), + sa.Column('expression_style', sa.Text(), nullable=False), + sa.Column('values', sa.Text(), nullable=False), + sa.Column('relationship_pattern', sa.Text(), nullable=False), + sa.Column('emotion_baseline', sa.Text(), nullable=False), + sa.Column('reason', sa.Text(), nullable=False), + sa.Column('evidence_ids_json', sa.Text(), nullable=False), + sa.Column('created_by', sa.String(length=80), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('rolled_back_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_personality_versions_character_id'), 'personality_versions', ['character_id'], unique=False) + op.create_table('role_permissions', + sa.Column('role_id', sa.String(length=40), nullable=False), + sa.Column('permission_id', sa.String(length=40), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('role_id', 'permission_id') + ) + op.create_table('time_anniversaries', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('character_id', sa.String(length=80), nullable=False), + sa.Column('name', sa.String(length=200), nullable=False), + sa.Column('calendar', sa.String(length=20), nullable=False), + sa.Column('date', sa.String(length=20), nullable=False), + sa.Column('recurs_yearly', sa.Boolean(), nullable=False), + sa.Column('kind', sa.String(length=40), nullable=False), + sa.Column('subject_type', sa.String(length=40), nullable=False), + sa.Column('source', sa.String(length=40), nullable=False), + sa.Column('metadata_json', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_time_anniversaries_character_id'), 'time_anniversaries', ['character_id'], unique=False) + op.create_table('user_roles', + sa.Column('user_id', sa.String(length=40), nullable=False), + sa.Column('role_id', sa.String(length=40), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('user_id', 'role_id') + ) + op.create_table('character_profile_candidates', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('character_id', sa.String(length=80), nullable=False), + sa.Column('import_job_id', sa.String(length=80), nullable=True), + sa.Column('field', sa.String(length=60), nullable=False), + sa.Column('value', sa.Text(), nullable=False), + sa.Column('confidence', sa.Float(), nullable=False), + sa.Column('evidence_ids_json', sa.Text(), nullable=False), + sa.Column('status', sa.String(length=40), nullable=False), + sa.Column('reason', sa.Text(), nullable=True), + sa.Column('reviewed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.ForeignKeyConstraint(['import_job_id'], ['character_import_jobs.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_character_profile_candidates_character_id'), 'character_profile_candidates', ['character_id'], unique=False) + op.create_index(op.f('ix_character_profile_candidates_import_job_id'), 'character_profile_candidates', ['import_job_id'], unique=False) + op.create_index(op.f('ix_character_profile_candidates_status'), 'character_profile_candidates', ['status'], unique=False) + op.create_table('curiosity_questions', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('character_id', sa.String(length=80), nullable=False), + sa.Column('cycle_id', sa.String(length=80), nullable=False), + sa.Column('question', sa.Text(), nullable=False), + sa.Column('source', sa.String(length=60), nullable=False), + sa.Column('related_entities_json', sa.Text(), nullable=False), + sa.Column('expected_value', sa.Float(), nullable=False), + sa.Column('urgency', sa.Float(), nullable=False), + sa.Column('risk_level', sa.String(length=8), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.ForeignKeyConstraint(['cycle_id'], ['big_cycles.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_curiosity_questions_character_id'), 'curiosity_questions', ['character_id'], unique=False) + op.create_index(op.f('ix_curiosity_questions_cycle_id'), 'curiosity_questions', ['cycle_id'], unique=False) + op.create_table('evolution_candidates', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('character_id', sa.String(length=80), nullable=False), + sa.Column('cycle_id', sa.String(length=80), nullable=False), + sa.Column('type', sa.String(length=60), nullable=False), + sa.Column('source', sa.String(length=60), nullable=False), + sa.Column('level', sa.String(length=8), nullable=False), + sa.Column('scope', sa.String(length=40), nullable=False), + sa.Column('title', sa.String(length=200), nullable=False), + sa.Column('proposal', sa.Text(), nullable=False), + sa.Column('delta_json', sa.Text(), nullable=False), + sa.Column('evidence_ids_json', sa.Text(), nullable=False), + sa.Column('confidence', sa.Float(), nullable=False), + sa.Column('benefit_score', sa.Float(), nullable=False), + sa.Column('risk_score', sa.Float(), nullable=False), + sa.Column('drift_score', sa.Float(), nullable=False), + sa.Column('auto_apply_allowed', sa.Boolean(), nullable=False), + sa.Column('status', sa.String(length=40), nullable=False), + sa.Column('resulting_target_id', sa.String(length=80), nullable=True), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('reviewed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('reviewed_by', sa.String(length=80), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.ForeignKeyConstraint(['cycle_id'], ['big_cycles.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_evolution_candidates_character_id'), 'evolution_candidates', ['character_id'], unique=False) + op.create_index(op.f('ix_evolution_candidates_cycle_id'), 'evolution_candidates', ['cycle_id'], unique=False) + op.create_index(op.f('ix_evolution_candidates_status'), 'evolution_candidates', ['status'], unique=False) + op.create_table('import_items', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('import_job_id', sa.String(length=80), nullable=False), + sa.Column('character_id', sa.String(length=80), nullable=False), + sa.Column('item_type', sa.String(length=40), nullable=False), + sa.Column('role', sa.String(length=40), nullable=False), + sa.Column('sequence', sa.Integer(), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('metadata_json', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.ForeignKeyConstraint(['import_job_id'], ['character_import_jobs.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_import_items_character_id'), 'import_items', ['character_id'], unique=False) + op.create_index(op.f('ix_import_items_import_job_id'), 'import_items', ['import_job_id'], unique=False) + op.create_table('lorebook_entries', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('lorebook_id', sa.String(length=80), nullable=False), + sa.Column('keys_json', sa.Text(), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('secondary_keys_json', sa.Text(), nullable=False), + sa.Column('constant', sa.Boolean(), nullable=False), + sa.Column('selective', sa.Boolean(), nullable=False), + sa.Column('enabled', sa.Boolean(), nullable=False), + sa.Column('case_sensitive', sa.Boolean(), nullable=False), + sa.Column('use_regex', sa.Boolean(), nullable=False), + sa.Column('position', sa.String(length=40), nullable=False), + sa.Column('insertion_order', sa.Integer(), nullable=False), + sa.Column('priority', sa.Integer(), nullable=False), + sa.Column('comment', sa.String(length=500), nullable=False), + sa.Column('bound_entity_id', sa.String(length=80), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['lorebook_id'], ['lorebooks.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_lorebook_entries_bound_entity_id'), 'lorebook_entries', ['bound_entity_id'], unique=False) + op.create_index(op.f('ix_lorebook_entries_lorebook_id'), 'lorebook_entries', ['lorebook_id'], unique=False) + op.create_table('memory_candidates', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('character_id', sa.String(length=80), nullable=False), + sa.Column('import_job_id', sa.String(length=80), nullable=True), + sa.Column('cycle_id', sa.String(length=80), nullable=True), + sa.Column('type', sa.String(length=60), nullable=False), + sa.Column('title', sa.String(length=200), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('confidence', sa.Float(), nullable=False), + sa.Column('importance', sa.Float(), nullable=False), + sa.Column('sensitivity', sa.String(length=40), nullable=False), + sa.Column('evidence_ids_json', sa.Text(), nullable=False), + sa.Column('status', sa.String(length=40), nullable=False), + sa.Column('reason', sa.Text(), nullable=True), + sa.Column('reviewed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.ForeignKeyConstraint(['cycle_id'], ['big_cycles.id'], ), + sa.ForeignKeyConstraint(['import_job_id'], ['character_import_jobs.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_memory_candidates_character_id'), 'memory_candidates', ['character_id'], unique=False) + op.create_index(op.f('ix_memory_candidates_cycle_id'), 'memory_candidates', ['cycle_id'], unique=False) + op.create_index(op.f('ix_memory_candidates_import_job_id'), 'memory_candidates', ['import_job_id'], unique=False) + op.create_index(op.f('ix_memory_candidates_status'), 'memory_candidates', ['status'], unique=False) + op.create_table('memory_links', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('character_id', sa.String(length=80), nullable=False), + sa.Column('source_memory_id', sa.String(length=80), nullable=False), + sa.Column('target_memory_id', sa.String(length=80), nullable=False), + sa.Column('relation', sa.String(length=80), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.ForeignKeyConstraint(['source_memory_id'], ['memories.id'], ), + sa.ForeignKeyConstraint(['target_memory_id'], ['memories.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_memory_links_character_id'), 'memory_links', ['character_id'], unique=False) + op.create_index(op.f('ix_memory_links_source_memory_id'), 'memory_links', ['source_memory_id'], unique=False) + op.create_index(op.f('ix_memory_links_target_memory_id'), 'memory_links', ['target_memory_id'], unique=False) + op.create_table('personality_proposals', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('character_id', sa.String(length=80), nullable=False), + sa.Column('fields_json', sa.Text(), nullable=False), + sa.Column('reason', sa.Text(), nullable=False), + sa.Column('status', sa.String(length=40), nullable=False), + sa.Column('resulting_version_id', sa.String(length=80), nullable=True), + sa.Column('reviewed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('reviewed_by', sa.String(length=80), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.ForeignKeyConstraint(['resulting_version_id'], ['personality_versions.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_personality_proposals_character_id'), 'personality_proposals', ['character_id'], unique=False) + op.create_index(op.f('ix_personality_proposals_status'), 'personality_proposals', ['status'], unique=False) + op.create_table('plans', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('goal_id', sa.String(length=80), nullable=True), + sa.Column('title', sa.String(length=300), nullable=False), + sa.Column('status', sa.String(length=40), nullable=False), + sa.Column('assumptions_json', sa.Text(), nullable=False), + sa.Column('risks_json', sa.Text(), nullable=False), + sa.Column('required_tools_json', sa.Text(), nullable=False), + sa.Column('success_criteria_json', sa.Text(), nullable=False), + sa.Column('step_count', sa.Integer(), nullable=False), + sa.Column('completed_steps', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['goal_id'], ['goals.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_plans_goal_id'), 'plans', ['goal_id'], unique=False) + op.create_index(op.f('ix_plans_status'), 'plans', ['status'], unique=False) + op.create_table('runs', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('character_id', sa.String(length=80), nullable=False), + sa.Column('session_id', sa.String(length=80), nullable=True), + sa.Column('status', sa.String(length=40), nullable=False), + sa.Column('goal', sa.String(length=300), nullable=True), + sa.Column('origin', sa.String(length=40), nullable=False), + sa.Column('model_hint', sa.String(length=120), nullable=True), + sa.Column('started_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('error_code', sa.String(length=80), nullable=True), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.ForeignKeyConstraint(['session_id'], ['chat_sessions.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_runs_character_id'), 'runs', ['character_id'], unique=False) + op.create_index(op.f('ix_runs_session_id'), 'runs', ['session_id'], unique=False) + op.create_index(op.f('ix_runs_status'), 'runs', ['status'], unique=False) + op.create_table('world_relations', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('character_id', sa.String(length=80), nullable=False), + sa.Column('subject_id', sa.String(length=80), nullable=False), + sa.Column('predicate', sa.String(length=120), nullable=False), + sa.Column('object_id', sa.String(length=80), nullable=False), + sa.Column('confidence', sa.Float(), nullable=False), + sa.Column('evidence_ids_json', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.ForeignKeyConstraint(['object_id'], ['character_world_entities.id'], ), + sa.ForeignKeyConstraint(['subject_id'], ['character_world_entities.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_world_relations_character_id'), 'world_relations', ['character_id'], unique=False) + op.create_index(op.f('ix_world_relations_object_id'), 'world_relations', ['object_id'], unique=False) + op.create_index(op.f('ix_world_relations_subject_id'), 'world_relations', ['subject_id'], unique=False) + op.create_table('audit_events', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('pivot', sa.String(length=40), nullable=True), + sa.Column('character_id', sa.String(length=80), nullable=True), + sa.Column('run_id', sa.String(length=80), nullable=True), + sa.Column('action', sa.String(length=120), nullable=False), + sa.Column('actor', sa.String(length=80), nullable=False), + sa.Column('risk_level', sa.String(length=20), nullable=False), + sa.Column('decision', sa.String(length=40), nullable=False), + sa.Column('reason', sa.Text(), nullable=True), + sa.Column('input_summary', sa.Text(), nullable=True), + sa.Column('output_summary', sa.Text(), nullable=True), + sa.Column('trace_id', sa.String(length=80), nullable=True), + sa.Column('resource_type', sa.String(length=80), nullable=True), + sa.Column('resource_id', sa.String(length=120), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.ForeignKeyConstraint(['run_id'], ['runs.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_audit_events_character_id'), 'audit_events', ['character_id'], unique=False) + op.create_index(op.f('ix_audit_events_pivot'), 'audit_events', ['pivot'], unique=False) + op.create_index(op.f('ix_audit_events_run_id'), 'audit_events', ['run_id'], unique=False) + op.create_index(op.f('ix_audit_events_trace_id'), 'audit_events', ['trace_id'], unique=False) + op.create_table('chat_messages', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('character_id', sa.String(length=80), nullable=False), + sa.Column('session_id', sa.String(length=80), nullable=False), + sa.Column('role', sa.String(length=40), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('modality', sa.String(length=40), nullable=False), + sa.Column('run_id', sa.String(length=80), nullable=True), + sa.Column('metadata_json', sa.Text(), nullable=False), + sa.Column('sensitivity', sa.String(length=40), nullable=False), + sa.Column('versions_json', sa.Text(), nullable=False, comment='版本历史 JSON 数组:[{content, created_at}],按生成顺序排列,末尾最新'), + sa.Column('active_version_idx', sa.Integer(), nullable=False, comment='当前活跃版本在 versions_json 中的索引(0-based)'), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.ForeignKeyConstraint(['run_id'], ['runs.id'], ), + sa.ForeignKeyConstraint(['session_id'], ['chat_sessions.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_chat_messages_character_id'), 'chat_messages', ['character_id'], unique=False) + op.create_index(op.f('ix_chat_messages_run_id'), 'chat_messages', ['run_id'], unique=False) + op.create_index(op.f('ix_chat_messages_session_id'), 'chat_messages', ['session_id'], unique=False) + op.create_table('events', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('character_id', sa.String(length=80), nullable=True), + sa.Column('run_id', sa.String(length=80), nullable=True), + sa.Column('type', sa.String(length=80), nullable=False), + sa.Column('source', sa.String(length=40), nullable=False), + sa.Column('payload_json', sa.Text(), nullable=False), + sa.Column('visibility', sa.String(length=40), nullable=False), + sa.Column('sensitivity', sa.String(length=40), nullable=False), + sa.Column('trace_id', sa.String(length=80), nullable=True), + sa.Column('parent_id', sa.String(length=80), nullable=True), + sa.Column('sequence', sa.Integer(), nullable=False), + sa.Column('evidence_ids_json', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.ForeignKeyConstraint(['run_id'], ['runs.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('run_id', 'sequence', name='uq_events_run_sequence') + ) + op.create_index(op.f('ix_events_character_id'), 'events', ['character_id'], unique=False) + op.create_index(op.f('ix_events_run_id'), 'events', ['run_id'], unique=False) + op.create_index(op.f('ix_events_trace_id'), 'events', ['trace_id'], unique=False) + op.create_index(op.f('ix_events_type'), 'events', ['type'], unique=False) + op.create_table('exploratory_subgoals', + sa.Column('id', sa.String(length=80), nullable=False), + sa.Column('character_id', sa.String(length=80), nullable=False), + sa.Column('cycle_id', sa.String(length=80), nullable=False), + sa.Column('parent_goal_id', sa.String(length=80), nullable=True), + sa.Column('curiosity_question_id', sa.String(length=80), nullable=False), + sa.Column('title', sa.String(length=200), nullable=False), + sa.Column('description', sa.Text(), nullable=False), + sa.Column('expected_information_gain', sa.Float(), nullable=False), + sa.Column('interruption_cost', sa.Float(), nullable=False), + sa.Column('risk_level', sa.String(length=8), nullable=False), + sa.Column('status', sa.String(length=40), nullable=False), + sa.Column('reviewed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('reviewed_by', sa.String(length=80), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ), + sa.ForeignKeyConstraint(['curiosity_question_id'], ['curiosity_questions.id'], ), + sa.ForeignKeyConstraint(['cycle_id'], ['big_cycles.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_exploratory_subgoals_character_id'), 'exploratory_subgoals', ['character_id'], unique=False) + op.create_index(op.f('ix_exploratory_subgoals_curiosity_question_id'), 'exploratory_subgoals', ['curiosity_question_id'], unique=False) + op.create_index(op.f('ix_exploratory_subgoals_cycle_id'), 'exploratory_subgoals', ['cycle_id'], unique=False) + op.create_index(op.f('ix_exploratory_subgoals_status'), 'exploratory_subgoals', ['status'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_exploratory_subgoals_status'), table_name='exploratory_subgoals') + op.drop_index(op.f('ix_exploratory_subgoals_cycle_id'), table_name='exploratory_subgoals') + op.drop_index(op.f('ix_exploratory_subgoals_curiosity_question_id'), table_name='exploratory_subgoals') + op.drop_index(op.f('ix_exploratory_subgoals_character_id'), table_name='exploratory_subgoals') + op.drop_table('exploratory_subgoals') + op.drop_index(op.f('ix_events_type'), table_name='events') + op.drop_index(op.f('ix_events_trace_id'), table_name='events') + op.drop_index(op.f('ix_events_run_id'), table_name='events') + op.drop_index(op.f('ix_events_character_id'), table_name='events') + op.drop_table('events') + op.drop_index(op.f('ix_chat_messages_session_id'), table_name='chat_messages') + op.drop_index(op.f('ix_chat_messages_run_id'), table_name='chat_messages') + op.drop_index(op.f('ix_chat_messages_character_id'), table_name='chat_messages') + op.drop_table('chat_messages') + op.drop_index(op.f('ix_audit_events_trace_id'), table_name='audit_events') + op.drop_index(op.f('ix_audit_events_run_id'), table_name='audit_events') + op.drop_index(op.f('ix_audit_events_pivot'), table_name='audit_events') + op.drop_index(op.f('ix_audit_events_character_id'), table_name='audit_events') + op.drop_table('audit_events') + op.drop_index(op.f('ix_world_relations_subject_id'), table_name='world_relations') + op.drop_index(op.f('ix_world_relations_object_id'), table_name='world_relations') + op.drop_index(op.f('ix_world_relations_character_id'), table_name='world_relations') + op.drop_table('world_relations') + op.drop_index(op.f('ix_runs_status'), table_name='runs') + op.drop_index(op.f('ix_runs_session_id'), table_name='runs') + op.drop_index(op.f('ix_runs_character_id'), table_name='runs') + op.drop_table('runs') + op.drop_index(op.f('ix_plans_status'), table_name='plans') + op.drop_index(op.f('ix_plans_goal_id'), table_name='plans') + op.drop_table('plans') + op.drop_index(op.f('ix_personality_proposals_status'), table_name='personality_proposals') + op.drop_index(op.f('ix_personality_proposals_character_id'), table_name='personality_proposals') + op.drop_table('personality_proposals') + op.drop_index(op.f('ix_memory_links_target_memory_id'), table_name='memory_links') + op.drop_index(op.f('ix_memory_links_source_memory_id'), table_name='memory_links') + op.drop_index(op.f('ix_memory_links_character_id'), table_name='memory_links') + op.drop_table('memory_links') + op.drop_index(op.f('ix_memory_candidates_status'), table_name='memory_candidates') + op.drop_index(op.f('ix_memory_candidates_import_job_id'), table_name='memory_candidates') + op.drop_index(op.f('ix_memory_candidates_cycle_id'), table_name='memory_candidates') + op.drop_index(op.f('ix_memory_candidates_character_id'), table_name='memory_candidates') + op.drop_table('memory_candidates') + op.drop_index(op.f('ix_lorebook_entries_lorebook_id'), table_name='lorebook_entries') + op.drop_index(op.f('ix_lorebook_entries_bound_entity_id'), table_name='lorebook_entries') + op.drop_table('lorebook_entries') + op.drop_index(op.f('ix_import_items_import_job_id'), table_name='import_items') + op.drop_index(op.f('ix_import_items_character_id'), table_name='import_items') + op.drop_table('import_items') + op.drop_index(op.f('ix_evolution_candidates_status'), table_name='evolution_candidates') + op.drop_index(op.f('ix_evolution_candidates_cycle_id'), table_name='evolution_candidates') + op.drop_index(op.f('ix_evolution_candidates_character_id'), table_name='evolution_candidates') + op.drop_table('evolution_candidates') + op.drop_index(op.f('ix_curiosity_questions_cycle_id'), table_name='curiosity_questions') + op.drop_index(op.f('ix_curiosity_questions_character_id'), table_name='curiosity_questions') + op.drop_table('curiosity_questions') + op.drop_index(op.f('ix_character_profile_candidates_status'), table_name='character_profile_candidates') + op.drop_index(op.f('ix_character_profile_candidates_import_job_id'), table_name='character_profile_candidates') + op.drop_index(op.f('ix_character_profile_candidates_character_id'), table_name='character_profile_candidates') + op.drop_table('character_profile_candidates') + op.drop_table('user_roles') + op.drop_index(op.f('ix_time_anniversaries_character_id'), table_name='time_anniversaries') + op.drop_table('time_anniversaries') + op.drop_table('role_permissions') + op.drop_index(op.f('ix_personality_versions_character_id'), table_name='personality_versions') + op.drop_table('personality_versions') + op.drop_index(op.f('ix_memories_status'), table_name='memories') + op.drop_index(op.f('ix_memories_character_id'), table_name='memories') + op.drop_table('memories') + op.drop_index(op.f('ix_lorebooks_status'), table_name='lorebooks') + op.drop_index(op.f('ix_lorebooks_scope'), table_name='lorebooks') + op.drop_index(op.f('ix_lorebooks_owner_scope'), table_name='lorebooks') + op.drop_index(op.f('ix_lorebooks_character_id'), table_name='lorebooks') + op.drop_table('lorebooks') + op.drop_index(op.f('ix_goals_status'), table_name='goals') + op.drop_index(op.f('ix_goals_level'), table_name='goals') + op.drop_index(op.f('ix_goals_character_id'), table_name='goals') + op.drop_table('goals') + op.drop_index(op.f('ix_chat_sessions_status'), table_name='chat_sessions') + op.drop_index(op.f('ix_chat_sessions_owner_scope'), table_name='chat_sessions') + op.drop_index(op.f('ix_chat_sessions_character_id'), table_name='chat_sessions') + op.drop_table('chat_sessions') + op.drop_index(op.f('ix_character_world_entities_type'), table_name='character_world_entities') + op.drop_index(op.f('ix_character_world_entities_status'), table_name='character_world_entities') + op.drop_index(op.f('ix_character_world_entities_character_id'), table_name='character_world_entities') + op.drop_table('character_world_entities') + op.drop_index(op.f('ix_character_import_jobs_status'), table_name='character_import_jobs') + op.drop_index(op.f('ix_character_import_jobs_material_type'), table_name='character_import_jobs') + op.drop_index(op.f('ix_character_import_jobs_checksum'), table_name='character_import_jobs') + op.drop_index(op.f('ix_character_import_jobs_character_id'), table_name='character_import_jobs') + op.drop_table('character_import_jobs') + op.drop_table('character_emotion_states') + op.drop_index(op.f('ix_big_cycles_status'), table_name='big_cycles') + op.drop_index(op.f('ix_big_cycles_completed_at'), table_name='big_cycles') + op.drop_index(op.f('ix_big_cycles_character_id'), table_name='big_cycles') + op.drop_table('big_cycles') + op.drop_index(op.f('ix_world_entities_type'), table_name='world_entities') + op.drop_table('world_entities') + op.drop_index(op.f('ix_users_username'), table_name='users') + op.drop_table('users') + op.drop_table('tool_policies') + op.drop_index(op.f('ix_skills_status'), table_name='skills') + op.drop_table('skills') + op.drop_index(op.f('ix_roles_name'), table_name='roles') + op.drop_table('roles') + op.drop_index(op.f('ix_permissions_resource'), table_name='permissions') + op.drop_table('permissions') + op.drop_table('perception_channels') + op.drop_index(op.f('ix_pending_confirmations_status'), table_name='pending_confirmations') + op.drop_index(op.f('ix_pending_confirmations_run_id'), table_name='pending_confirmations') + op.drop_index(op.f('ix_pending_confirmations_character_id'), table_name='pending_confirmations') + op.drop_table('pending_confirmations') + op.drop_table('model_endpoint_overrides') + op.drop_index(op.f('ix_mcp_servers_status'), table_name='mcp_servers') + op.drop_table('mcp_servers') + op.drop_index(op.f('ix_idempotency_keys_trace_id'), table_name='idempotency_keys') + op.drop_index(op.f('ix_idempotency_keys_scope'), table_name='idempotency_keys') + op.drop_index(op.f('ix_idempotency_keys_key'), table_name='idempotency_keys') + op.drop_index(op.f('ix_idempotency_keys_expires_at'), table_name='idempotency_keys') + op.drop_table('idempotency_keys') + op.drop_table('generation_preferences') + op.drop_index(op.f('ix_characters_template_key'), table_name='characters') + op.drop_index(op.f('ix_characters_status'), table_name='characters') + op.drop_index(op.f('ix_characters_source_template_id'), table_name='characters') + op.drop_index(op.f('ix_characters_owner_scope'), table_name='characters') + op.drop_index(op.f('ix_characters_is_system_template'), table_name='characters') + op.drop_table('characters') + op.drop_table('agent_roster') + # ### end Alembic commands ### diff --git a/backend/app/api/router.py b/backend/app/api/router.py index edbdc3a..2ea0e9f 100644 --- a/backend/app/api/router.py +++ b/backend/app/api/router.py @@ -17,6 +17,7 @@ from app.api.v1.endpoints import ( goals, health, imports, + kernel_runs, lorebooks, mcp, memory, @@ -38,6 +39,7 @@ api_router.include_router(characters.router) api_router.include_router(character_import.router) api_router.include_router(chat.router) api_router.include_router(runs.router) +api_router.include_router(kernel_runs.router) api_router.include_router(memory.router) api_router.include_router(lorebooks.router) api_router.include_router(personality.router) diff --git a/backend/app/api/v1/endpoints/kernel_runs.py b/backend/app/api/v1/endpoints/kernel_runs.py new file mode 100644 index 0000000..3b201d2 --- /dev/null +++ b/backend/app/api/v1/endpoints/kernel_runs.py @@ -0,0 +1,55 @@ +"""Kernel Run API(rewrite/1.0 新契约首个端点)。 + +POST /api/v1/runs:创建占位前台 Run(M0 骨架),事件流经既有 +GET /api/v1/runs/{run_id}/stream 订阅(SSE + Last-Event-ID 断线续传)。 +与旧 runs 路由共存:本模块只新增 POST,不改动旧读端点(M2 重建时收敛)。 +""" + +from __future__ import annotations + +from fastapi import APIRouter, Depends, Request +from pydantic import BaseModel, Field +from sqlalchemy.ext.asyncio import AsyncSession + +from app.api.v1.deps import get_current_user +from app.db.session import get_session +from app.kernel import Kernel +from app.schemas.auth import CurrentUser +from app.schemas.runtime import RunResponse +from app.services.characters import resolve_active_character +from app.services.runtime import get_run_response + +router = APIRouter(prefix="/runs", tags=["kernel"]) + + +class KernelRunCreate(BaseModel): + """占位 Run 创建请求:M0 无镜树挂载,prompt 直接作为用户输入。""" + + prompt: str = Field(default="你好,请介绍一下你自己。", min_length=1, max_length=8000) + character_id: str | None = Field(default=None, description="缺省用当前激活人物") + + +def _get_kernel(request: Request) -> Kernel: + kernel: Kernel | None = getattr(request.app.state, "kernel", None) + if kernel is None: + from app.core.errors import AppError + + raise AppError("service_unavailable", "Kernel 运行时未就绪。", status_code=503) + return kernel + + +@router.post("", response_model=RunResponse, status_code=200) +async def create_kernel_run( + payload: KernelRunCreate, + request: Request, + user: CurrentUser = Depends(get_current_user), + db: AsyncSession = Depends(get_session), +) -> RunResponse: + kernel = _get_kernel(request) + character = await resolve_active_character( + db, payload.character_id, owner_scope=user.owner_scope + ) + run = await kernel.start_foreground_run( + character_id=character.id, prompt=payload.prompt + ) + return await get_run_response(run.id) diff --git a/backend/app/core/config/runtime_config.py b/backend/app/core/config/runtime_config.py index 28f5bb0..d170739 100644 --- a/backend/app/core/config/runtime_config.py +++ b/backend/app/core/config/runtime_config.py @@ -14,6 +14,11 @@ class RuntimeConfig(BaseSettings): sse_poll_interval_seconds: float = 0.01 # SSE 轮询间隔(秒,10ms:前端逐段平滑增量) sse_heartbeat_seconds: float = 15.0 # SSE 心跳间隔(秒) run_timeout_seconds: int = 120 # 运行超时时间(秒) + + # ==================== 神识(模型网关,rewrite/1.0) ==================== + shenshi_base_url: str = "" # 神识服务地址;为空时 Kernel 使用进程内回声桩(M0 骨架) + shenshi_service_token: str = "" # 灵台→神识服务令牌(开发档可空) + shenshi_timeout_seconds: float = 120.0 # 神识流式调用超时(秒) # Agent 循环最大轮数:模型每轮仍带工具调用则继续,超限截断返回(见 agent.runtime.DEFAULT_MAX_ITERATIONS) agent_max_iterations: int = 8 # 单次高风险工具确认挂起超时(秒):须小于 run_timeout_seconds,超时按拒绝处理; diff --git a/backend/app/kernel/__init__.py b/backend/app/kernel/__init__.py new file mode 100644 index 0000000..fbafeda --- /dev/null +++ b/backend/app/kernel/__init__.py @@ -0,0 +1,78 @@ +"""极小硬核 Kernel:不变量裁决、命台并发控制、事件日志写入、Run 循环骨架。 + +不可插拔(lingtai-runtime.md §1);插件与 Profile 见 ``app.runtime``。 +""" + +from __future__ import annotations + +import asyncio + +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker + +from app.kernel.eventlog import KernelEventLog +from app.kernel.loop import KernelRunLoop +from app.kernel.seat import SeatManager +from app.models.runtime import Run +from app.runtime.bus import WaterfallBus +from app.runtime.profile import resolve_profile +from app.shenshi_client.client import ShenshiClient + +__all__ = ["Kernel", "KernelEventLog", "KernelRunLoop", "SeatManager"] + + +class Kernel: + """门面:持有 Kernel 全部部件,供 lifespan 装配与 API 层调用。""" + + def __init__( + self, + *, + shenshi: ShenshiClient, + session_factory: async_sessionmaker[AsyncSession], + event_log: KernelEventLog, + bus: WaterfallBus | None = None, + seats: SeatManager | None = None, + timeout_seconds: float = 120.0, + ) -> None: + self._sessions = session_factory + self.bus = bus or WaterfallBus() + self.seats = seats or SeatManager() + self.loop = KernelRunLoop( + event_log=event_log, + seats=self.seats, + bus=self.bus, + shenshi=shenshi, + session_factory=session_factory, + timeout_seconds=timeout_seconds, + ) + self._tasks: set[asyncio.Task[None]] = set() + + async def start_foreground_run( + self, *, character_id: str, prompt: str, trace_id: str | None = None + ) -> Run: + """创建 Run 行(queued)并异步启动循环;前台席位在循环内排队获取。""" + profile = resolve_profile() + async with self._sessions() as session: + run = Run( + character_id=character_id, + session_id=None, + status="queued", + goal=prompt[:300] if prompt else None, + origin="kernel", + model_hint=str(profile.params.get("slot", "chat")), + ) + session.add(run) + await session.commit() + run_id = run.id + + task = asyncio.create_task( + self.loop.execute( + run_id=run_id, character_id=character_id, prompt=prompt, trace_id=trace_id + ) + ) + self._tasks.add(task) + task.add_done_callback(self._tasks.discard) + + async with self._sessions() as session: + created = await session.get(Run, run_id) + assert created is not None + return created diff --git a/backend/app/kernel/eventlog.py b/backend/app/kernel/eventlog.py new file mode 100644 index 0000000..aa85ed2 --- /dev/null +++ b/backend/app/kernel/eventlog.py @@ -0,0 +1,63 @@ +"""Kernel 事件日志:一炁(EventStore)之上的 Kernel 写入接口。 + +- 落库走 ``DbEventStore``(run 内单调 sequence,DB 为事实源,SSE 断线重放依据); +- 双发进程内 ``bus``(RunEventBus)供既有 ``/runs/{id}/stream`` SSE 实时转发。 + 该双发是 M0 骨架的过渡接法,M2 重建 runs 路由时收敛为 EventStore 单通道。 +""" + +from __future__ import annotations + +from typing import Any + +from app.modules.runtime.events import Event, EventStore +from app.schemas.runtime import EventResponse +from app.services.event_bus import bus + + +class KernelEventLog: + """Kernel 视角的事件写入与检索。""" + + def __init__(self, store: EventStore) -> None: + self._store = store + + async def emit( + self, + *, + type: str, # noqa: A002 — 对齐事件字段名 + run_id: str, + character_id: str, + source: str = "kernel", + payload: dict[str, Any] | None = None, + visibility: str = "public", + trace_id: str | None = None, + ) -> Event: + event = await self._store.append( + Event( + type=type, + source=source, + payload=payload or {}, + visibility=visibility, # type: ignore[arg-type] + run_id=run_id, + character_id=character_id, + trace_id=trace_id, + ) + ) + bus.publish(run_id, _to_response(event)) + return event + + async def list_by_run(self, run_id: str) -> list[Event]: + return await self._store.list_by_run(run_id) + + +def _to_response(event: Event) -> EventResponse: + return EventResponse( + event_id=event.id, + run_id=event.run_id or "", + character_id=event.character_id or "", + type=event.type, + sequence=event.sequence if event.sequence is not None else 0, + visibility=event.visibility, + payload=event.payload, + created_at=event.created_at, + trace_id=event.trace_id, + ) diff --git a/backend/app/kernel/invariants.py b/backend/app/kernel/invariants.py new file mode 100644 index 0000000..fb93ff9 --- /dev/null +++ b/backend/app/kernel/invariants.py @@ -0,0 +1,52 @@ +"""Kernel 不可插拔不变量(lingtai-runtime.md §1 硬核例外)。 + +M0 落地其中两条的可执行断言: +- "模型可见即可重建":投影中的每条模型可见消息必须能在事件日志(llm.requested)中找到; +- 流一致性:llm.delta 事件重组文本必须与进入接纳检查的文本一致。 + +镜树不变量(重生成不覆盖/未归真不入忆/归真不回滚)随 M2 镜树域落地时进入本模块; +禁域裁决点随 M5 司命落地。断言失败即抛 ``InvariantViolation``:Kernel 视其为缺陷 +而非业务错误,Run 进入 interrupted 并带 HJ-9000 段错误码。 +""" + +from __future__ import annotations + +from app.modules.runtime.events import Event +from app.shenshi_client.types import ShenshiChatRequest + + +class InvariantViolation(Exception): + """Kernel 不变量被违反:缺陷信号,必须中断并留痕。""" + + def __init__(self, invariant: str, detail: str) -> None: + super().__init__(f"[{invariant}] {detail}") + self.invariant = invariant + self.detail = detail + + +def assert_model_visible_logged( + request: ShenshiChatRequest, events: list[Event] +) -> None: + """投影消息 ⊆ 已记录的 llm.requested 消息("模型可见即可重建")。""" + requested = next((e for e in events if e.type == "llm.requested"), None) + if requested is None: + raise InvariantViolation( + "model-visible-means-logged", "存在模型请求但事件日志缺少 llm.requested" + ) + logged = requested.payload.get("request", {}).get("messages", []) + if logged != [m.model_dump() for m in request.messages]: + raise InvariantViolation( + "model-visible-means-logged", "llm.requested 事件与实际发送的 IR 消息不一致" + ) + + +def assert_stream_reconstruction(events: list[Event], accepted_text: str) -> None: + """llm.delta 重组文本 == 进入接纳的文本(流不丢帧、不重帧)。""" + reconstructed = "".join( + e.payload.get("delta", "") for e in events if e.type == "llm.delta" + ) + if reconstructed != accepted_text: + raise InvariantViolation( + "stream-reconstruction", + f"事件重组文本与接纳文本不一致:{len(reconstructed)} != {len(accepted_text)} 字符", + ) diff --git a/backend/app/kernel/loop.py b/backend/app/kernel/loop.py new file mode 100644 index 0000000..e0d8abf --- /dev/null +++ b/backend/app/kernel/loop.py @@ -0,0 +1,279 @@ +"""Kernel Run 循环骨架(M0 占位形态)。 + +事件序列对应 docs/architecture/lingtai-runtime.md §3.1;瀑布节点可被插件否决。 +M0 的投影为固定占位(系统契约占位 + 用户输入),M2 起接入镜树祖先路径、获准记忆 +与显式引文三类来源。 +""" + +from __future__ import annotations + +import asyncio +from dataclasses import replace +from datetime import datetime, UTC +from typing import Any + +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker + +from app.core.ids import new_run_id, new_trace_id +from app.kernel.eventlog import KernelEventLog +from app.kernel.invariants import ( + InvariantViolation, + assert_model_visible_logged, + assert_stream_reconstruction, +) +from app.kernel.seat import SeatManager +from app.models.runtime import Run +from app.runtime.bus import ( + NODE_LLM_PRE_REQUEST, + NODE_RESULT_PRE_ACCEPT, + NODE_RUN_REQUESTED, + NODE_TURN_STARTED, + RunContext, + Veto, + WaterfallBus, +) +from app.runtime.profile import Profile, resolve_profile +from app.shenshi_client.client import ShenshiClient +from app.shenshi_client.types import ShenshiMessage, ShenshiChatRequest + +_SYSTEM_CONTRACT_PLACEHOLDER = "你是魂镜人物运行时(M0 骨架占位契约)。" + + +class KernelRunLoop: + """单次 Run 的执行骨架:命台席位 → 事件序列 → 神识流 → 接纳断言。""" + + def __init__( + self, + *, + event_log: KernelEventLog, + seats: SeatManager, + bus: WaterfallBus, + shenshi: ShenshiClient, + session_factory: async_sessionmaker[AsyncSession], + timeout_seconds: float, + ) -> None: + self._log = event_log + self._seats = seats + self._bus = bus + self._shenshi = shenshi + self._sessions = session_factory + self._timeout = timeout_seconds + + async def execute( + self, *, run_id: str, character_id: str, prompt: str, trace_id: str | None = None + ) -> None: + trace = trace_id or new_trace_id() + try: + async with asyncio.timeout(self._timeout): + await self._execute( + run_id=run_id, character_id=character_id, prompt=prompt, trace_id=trace + ) + except TimeoutError: + await self._finish_interrupted( + run_id, character_id, trace, code="run_timeout", message="运行超时被中断" + ) + except asyncio.CancelledError: + await self._emit(run_id, character_id, trace, "run.cancelled", visibility="public") + await self._update_run(run_id, status="cancelled") + raise + + async def _execute( + self, *, run_id: str, character_id: str, prompt: str, trace_id: str + ) -> None: + ctx = RunContext(run_id=run_id, character_id=character_id, trace_id=trace_id) + try: + await self._bus.fire(NODE_RUN_REQUESTED, ctx) + except Veto as veto: + await self._record_veto(run_id, character_id, trace_id, veto) + await self._finish_interrupted( + run_id, character_id, trace_id, code="HJ-1301", message=veto.reason + ) + return + + await self._emit(run_id, character_id, trace_id, "run.queued") + async with self._seats.foreground(character_id): + await self._update_run(run_id, status="running") + await self._emit(run_id, character_id, trace_id, "run.started") + await self._turn(run_id, character_id, trace_id, prompt) + + async def _turn( + self, run_id: str, character_id: str, trace_id: str, prompt: str + ) -> None: + ctx = RunContext( + run_id=run_id, character_id=character_id, trace_id=trace_id, payload={"prompt": prompt} + ) + profile: Profile = resolve_profile() + await self._emit(run_id, character_id, trace_id, "turn.started") + await self._bus.fire(NODE_TURN_STARTED, ctx) + + request = ShenshiChatRequest( + model=profile.params.get("model"), + slot=str(profile.params.get("slot", "chat")), + messages=[ + ShenshiMessage(role="system", content=_SYSTEM_CONTRACT_PLACEHOLDER), + ShenshiMessage(role="user", content=prompt), + ], + run_id=run_id, + trace_id=trace_id, + character_id=character_id, + ) + await self._emit( + run_id, + character_id, + trace_id, + "context.projected", + payload={ + "profile": profile.snapshot(), + "sources": {"system_contract": "placeholder", "user_input": True}, + }, + visibility="internal", + ) + try: + await self._bus.fire(NODE_LLM_PRE_REQUEST, ctx) + except Veto as veto: + await self._record_veto(run_id, character_id, trace_id, veto) + await self._finish_interrupted( + run_id, character_id, trace_id, code="HJ-1301", message=veto.reason + ) + return + + # "模型可见即可重建"锚点:完整 IR 请求进事件日志 + await self._emit( + run_id, + character_id, + trace_id, + "llm.requested", + payload={"request": request.model_dump(mode="json")}, + visibility="internal", + ) + + text_parts: list[str] = [] + usage: dict[str, Any] = {} + try: + async for delta in self._shenshi.stream_chat(request): + if delta.delta: + text_parts.append(delta.delta) + await self._emit( + run_id, character_id, trace_id, "llm.delta", payload={"delta": delta.delta} + ) + if delta.finish_reason: + usage = { + "prompt_tokens": delta.usage_prompt_tokens, + "completion_tokens": delta.usage_completion_tokens, + "finish_reason": delta.finish_reason, + } + except Exception as exc: # 神识侧失败:如实上报,不伪装完成 + await self._emit( + run_id, + character_id, + trace_id, + "llm.failed", + payload={"error": str(exc)}, + visibility="internal", + ) + await self._finish_interrupted( + run_id, character_id, trace_id, code="HJ-2001", message=f"神识模型调用失败:{exc}" + ) + return + + await self._emit(run_id, character_id, trace_id, "llm.completed", payload={"usage": usage}) + + accepted_text = "".join(text_parts) + await self._bus.fire( + NODE_RESULT_PRE_ACCEPT, replace(ctx, payload={"result": accepted_text}) + ) + events = await self._log.list_by_run(run_id) + try: + assert_model_visible_logged(request, events) + assert_stream_reconstruction(events, accepted_text) + except InvariantViolation as violation: + await self._emit( + run_id, + character_id, + trace_id, + "run.invariant_violation", + payload={"invariant": violation.invariant, "detail": violation.detail}, + visibility="audit", + ) + await self._finish_interrupted( + run_id, + character_id, + trace_id, + code="HJ-9000", + message=f"Kernel 不变量违反:{violation.invariant}", + ) + return + + await self._emit( + run_id, character_id, trace_id, "result.accepted", payload={"content": accepted_text} + ) + await self._emit(run_id, character_id, trace_id, "turn.finished") + await self._emit(run_id, character_id, trace_id, "run.finished") + await self._update_run(run_id, status="completed") + + async def _record_veto( + self, run_id: str, character_id: str, trace_id: str, veto: Veto + ) -> None: + await self._emit( + run_id, + character_id, + trace_id, + "safety.ruled", + payload={"reason": veto.reason, "rule": veto.rule, "details": veto.details}, + visibility="audit", + ) + + async def _finish_interrupted( + self, + run_id: str, + character_id: str, + trace_id: str, + *, + code: str, + message: str, + ) -> None: + await self._emit(run_id, character_id, trace_id, "run.interrupted") + await self._update_run(run_id, status="interrupted", error_code=code, error_message=message) + + async def _emit( + self, + run_id: str, + character_id: str, + trace_id: str, + type: str, # noqa: A002 + payload: dict[str, Any] | None = None, + visibility: str = "public", + ) -> None: + await self._log.emit( + type=type, + run_id=run_id, + character_id=character_id, + trace_id=trace_id, + payload=payload, + visibility=visibility, + ) + + async def _update_run( + self, + run_id: str, + *, + status: str, + error_code: str | None = None, + error_message: str | None = None, + ) -> None: + async with self._sessions() as session: + run = await session.get(Run, run_id) + if run is None: + return + run.status = status + run.error_code = error_code + run.error_message = error_message + if status == "running": + run.started_at = datetime.now(UTC) + if status in ("completed", "failed", "cancelled", "interrupted"): + run.completed_at = datetime.now(UTC) + await session.commit() + + +def new_kernel_run_id() -> str: + return new_run_id() diff --git a/backend/app/kernel/seat.py b/backend/app/kernel/seat.py new file mode 100644 index 0000000..a07dd52 --- /dev/null +++ b/backend/app/kernel/seat.py @@ -0,0 +1,44 @@ +"""命台席位控制:同一人物同一时刻只有一条前台意识流。 + +M0 为进程内实现(单人物粒度 asyncio 锁 + 等待计数);多副本部署在标准档 +切换 Redis 租约(lingtai-runtime.md §8),接口保持不变。 +""" + +from __future__ import annotations + +from collections import defaultdict +from contextlib import asynccontextmanager +from collections.abc import AsyncIterator +import asyncio + + +class SeatManager: + """人物粒度的前台席位。后台受托 Run(kind=background_task)不占席位。""" + + def __init__(self) -> None: + self._locks: dict[str, asyncio.Lock] = {} + self._waiting: dict[str, int] = defaultdict(int) + + def _lock(self, character_id: str) -> asyncio.Lock: + if character_id not in self._locks: + self._locks[character_id] = asyncio.Lock() + return self._locks[character_id] + + @asynccontextmanager + async def foreground(self, character_id: str) -> AsyncIterator[None]: + """占用前台席位;同人物并发请求在此排队(Kernel 不变量 2)。""" + lock = self._lock(character_id) + if lock.locked(): + self._waiting[character_id] += 1 + try: + async with lock: + yield + finally: + self._waiting[character_id] -= 1 + else: + async with lock: + yield + + def queue_depth(self, character_id: str) -> int: + """该人物当前排队等待前台席位的请求数(HJ-1101 附带信息)。""" + return self._waiting.get(character_id, 0) diff --git a/backend/app/main.py b/backend/app/main.py index bdb4d47..d257b8c 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -86,6 +86,18 @@ async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]: # 事件流与历史查询不依赖沙箱,始终可用。 _app.state.event_store = DbEventStore(AsyncSessionLocal) + # Kernel(rewrite/1.0 M0):极小硬核 + 事件日志 + 前台席位;神识客户端按配置装配, + # 未配置神识地址时为进程内回声桩(骨架形态,见 app.shenshi_client)。 + from app.kernel import Kernel, KernelEventLog + from app.shenshi_client import build_shenshi_client + + _app.state.kernel = Kernel( + shenshi=build_shenshi_client(settings.shenshi_base_url, settings.shenshi_service_token), + session_factory=AsyncSessionLocal, + event_log=KernelEventLog(_app.state.event_store), + timeout_seconds=float(settings.run_timeout_seconds), + ) + # ── MCP 进程管理器(stdio 子进程生命周期管理) ────────────── from app.integrations.mcp.process_manager import ( ProcessManager, diff --git a/backend/app/runtime/__init__.py b/backend/app/runtime/__init__.py new file mode 100644 index 0000000..79053ce --- /dev/null +++ b/backend/app/runtime/__init__.py @@ -0,0 +1,16 @@ +"""灵台插件化运行时(事件总线 / 插件 / Profile)。Kernel 见 ``app.kernel``。""" + +from app.runtime.bus import RunContext, Veto, WaterfallBus +from app.runtime.plugin import Plugin, PluginRegistry +from app.runtime.profile import Profile, ProfileLayer, resolve_profile + +__all__ = [ + "Plugin", + "PluginRegistry", + "Profile", + "ProfileLayer", + "RunContext", + "Veto", + "WaterfallBus", + "resolve_profile", +] diff --git a/backend/app/runtime/bus.py b/backend/app/runtime/bus.py new file mode 100644 index 0000000..530f607 --- /dev/null +++ b/backend/app/runtime/bus.py @@ -0,0 +1,79 @@ +"""运行时事件总线:瀑布节点 + 可否决监听器。 + +设计依据 docs/architecture/lingtai-runtime.md §1/§2:注入、审计、安全都在事件层做, +不改循环骨架。监听器按注册序执行;任一监听器抛 ``Veto`` 即中断该节点(否决), +否决事实由调用方记录(safety.ruled 等事件),总线本身不落库。 +""" + +from __future__ import annotations + +import fnmatch +from collections import defaultdict +from collections.abc import Awaitable, Callable +from dataclasses import dataclass, field +from typing import Any + +# 瀑布节点名(契约词典 §8.3 的可否决子集) +NODE_RUN_REQUESTED = "run.requested" +NODE_TURN_STARTED = "turn.started" +NODE_LLM_PRE_REQUEST = "llm.pre_request" +NODE_TOOL_PRE_EXECUTE = "tool.pre_execute" +NODE_RESULT_PRE_ACCEPT = "result.pre_accept" + +WATERFALL_NODES = frozenset( + { + NODE_RUN_REQUESTED, + NODE_TURN_STARTED, + NODE_LLM_PRE_REQUEST, + NODE_TOOL_PRE_EXECUTE, + NODE_RESULT_PRE_ACCEPT, + } +) + + +class Veto(Exception): + """监听器否决:携带裁决摘要(reason / 规则标识)。""" + + def __init__(self, reason: str, *, rule: str = "", details: dict[str, Any] | None = None): + super().__init__(reason) + self.reason = reason + self.rule = rule + self.details = details or {} + + +@dataclass(frozen=True) +class RunContext: + """节点监听器可见的运行上下文(只读视图,变更经事件日志)。""" + + run_id: str + character_id: str + trace_id: str | None = None + payload: dict[str, Any] = field(default_factory=dict) + + +Listener = Callable[[str, RunContext], Awaitable[None]] + + +class WaterfallBus: + """瀑布节点订阅与触发。""" + + def __init__(self) -> None: + self._listeners: dict[str, list[Listener]] = defaultdict(list) + + def on(self, node: str, listener: Listener) -> None: + """注册节点监听器;支持 glob 通配(如 ``llm.*``)。""" + if not any(fnmatch.fnmatch(node, pattern) for pattern in WATERFALL_NODES): + raise ValueError(f"未知瀑布节点:{node}") + self._listeners[node].append(listener) + + async def fire(self, node: str, context: RunContext) -> None: + """按注册序触发监听器;任一否决即停止后续。 + + 精确键与通配键(如 ``llm.*``)各自成列表,列表内保持注册序; + 两种键之间的相对顺序不保证,需要严格顺序的场景用精确注册。 + """ + for key, listeners in self._listeners.items(): + if key != node and not fnmatch.fnmatch(node, key): + continue + for listener in list(listeners): + await listener(node, context) diff --git a/backend/app/runtime/plugin.py b/backend/app/runtime/plugin.py new file mode 100644 index 0000000..357e51d --- /dev/null +++ b/backend/app/runtime/plugin.py @@ -0,0 +1,42 @@ +"""插件契约与注册表。 + +一切能力插件化(docs/architecture/lingtai-runtime.md §4):插件只通过订阅瀑布节点 +与提供能力参与运行,彼此不直接调用。M0 只需注册表与装配骨架,首个真实插件(司命 +占位裁决器)随 M5 落地;禁域挂载点在 Kernel,不在插件。 +""" + +from __future__ import annotations + +from typing import Protocol, runtime_checkable + +from app.runtime.bus import WaterfallBus + + +@runtime_checkable +class Plugin(Protocol): + """插件契约:稳定标识 + 版本 + 注册入口。""" + + @property + def name(self) -> str: ... + + @property + def version(self) -> str: ... + + def register(self, bus: WaterfallBus) -> None: ... + + +class PluginRegistry: + """按注册序持有插件;同名插件后注册覆盖前注册(Profile 层叠语义)。""" + + def __init__(self) -> None: + self._plugins: dict[str, Plugin] = {} + + def register(self, plugin: Plugin) -> None: + self._plugins[plugin.name] = plugin + + def bootstrap(self, bus: WaterfallBus) -> None: + for plugin in list(self._plugins.values()): + plugin.register(bus) + + def names(self) -> list[str]: + return list(self._plugins) diff --git a/backend/app/runtime/profile.py b/backend/app/runtime/profile.py new file mode 100644 index 0000000..a8dd1ca --- /dev/null +++ b/backend/app/runtime/profile.py @@ -0,0 +1,42 @@ +"""Profile:一次 Run 的插件装配清单与参数包。 + +层叠顺序(产品 PRD §9.2 / lingtai-runtime.md §4.2): +平台硬边界 → 平台目录 → 用户偏好/BYOK → 人物覆写 → 镜树临时选择。 +M0 只有平台目录层;后续里程碑逐层接入。Profile 快照写入事件日志,保证可回放。 +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + + +@dataclass(frozen=True) +class ProfileLayer: + """层叠中的一层:来源标识 + 参数(后者覆盖前者同键)。""" + + source: str + params: dict[str, Any] = field(default_factory=dict) + + +_PLATFORM_LAYER = ProfileLayer(source="platform", params={"slot": "chat"}) + + +@dataclass(frozen=True) +class Profile: + """解析后的最终装配:插件名列表 + 合并参数。""" + + plugins: tuple[str, ...] = () + params: dict[str, Any] = field(default_factory=dict) + + def snapshot(self) -> dict[str, Any]: + """写入 context.projected 事件的快照视图。""" + return {"plugins": list(self.plugins), "params": dict(self.params)} + + +def resolve_profile(layers: list[ProfileLayer] | None = None) -> Profile: + """层叠解析:平台层恒在最底,逐层覆盖参数;M0 无插件装配。""" + merged: dict[str, Any] = {} + for layer in [_PLATFORM_LAYER, *(layers or [])]: + merged.update(layer.params) + return Profile(plugins=(), params=merged) diff --git a/backend/app/services/event_bus.py b/backend/app/services/event_bus.py index a82f7a2..4c37c1e 100644 --- a/backend/app/services/event_bus.py +++ b/backend/app/services/event_bus.py @@ -14,7 +14,15 @@ from collections import defaultdict from app.schemas.runtime import EventResponse -_TERMINAL_EVENT_TYPES = {"run.completed", "run.failed", "run.cancelled"} +# 终态事件:新旧两套运行时共用本总线转发 SSE;Kernel 契约(run.finished/run.interrupted) +# 与旧运行时(run.completed/run.failed)的终态都在此登记 +_TERMINAL_EVENT_TYPES = { + "run.completed", + "run.failed", + "run.cancelled", + "run.finished", + "run.interrupted", +} class RunEventBus: diff --git a/backend/app/shenshi_client/__init__.py b/backend/app/shenshi_client/__init__.py new file mode 100644 index 0000000..368bb0f --- /dev/null +++ b/backend/app/shenshi_client/__init__.py @@ -0,0 +1,25 @@ +"""神识客户端边界(灵台唯一模型出口)。""" + +from app.shenshi_client.client import ( + EchoShenshi, + HttpShenshiClient, + ShenshiClient, + build_shenshi_client, +) +from app.shenshi_client.types import ( + ShenshiChatRequest, + ShenshiDelta, + ShenshiError, + ShenshiMessage, +) + +__all__ = [ + "EchoShenshi", + "HttpShenshiClient", + "ShenshiChatRequest", + "ShenshiClient", + "ShenshiDelta", + "ShenshiError", + "ShenshiMessage", + "build_shenshi_client", +] diff --git a/backend/app/shenshi_client/client.py b/backend/app/shenshi_client/client.py new file mode 100644 index 0000000..5dcda1b --- /dev/null +++ b/backend/app/shenshi_client/client.py @@ -0,0 +1,93 @@ +"""神识客户端:灵台唯一的模型出口边界。 + +两种实现可互换(Protocol): +- ``HttpShenshiClient``:经 HTTP+SSE 调用 Go 神识服务(SHENSHI_BASE_URL 配置时启用); +- ``EchoShenshi``:进程内回声桩,逐字回流输入,供本地开发与测试在无神识时跑通端到端。 + +领域模块禁止绕过本边界 import Provider SDK(docs/architecture/service-boundaries.md §4)。 +""" + +from __future__ import annotations + +import json +from collections.abc import AsyncIterator +from typing import Protocol + +import httpx + +from app.shenshi_client.types import ShenshiChatRequest, ShenshiDelta, ShenshiError + + +class ShenshiClient(Protocol): + """神识数据面契约:流式对话补全。""" + + def stream_chat(self, request: ShenshiChatRequest) -> AsyncIterator[ShenshiDelta]: ... + + +class EchoShenshi: + """进程内回声桩:把最后一条用户消息按固定粒度切分回流。 + + 只用于 M0 骨架验证(登录 → 占位 Run → SSE 端到端),不模拟任何 Provider 行为。 + """ + + chunk_size = 8 + + async def stream_chat(self, request: ShenshiChatRequest) -> AsyncIterator[ShenshiDelta]: + prompt = next((m.content for m in reversed(request.messages) if m.role == "user"), "") + text = f"【回声桩】{prompt}" + for i in range(0, len(text), self.chunk_size): + yield ShenshiDelta(delta=text[i : i + self.chunk_size]) + yield ShenshiDelta( + finish_reason="stop", + usage_prompt_tokens=max(len(prompt) // 4, 1), + usage_completion_tokens=max(len(text) // 4, 1), + ) + + +class HttpShenshiClient: + """HTTP+SSE 神识客户端。 + + 认证:服务令牌经 ``Authorization: Bearer`` 透传(管理面 RBAC 不在此层)。 + 流解析:``data:`` 帧为 ShenshiDelta JSON,``[DONE]`` 结束;非 2xx 映射 ShenshiError。 + """ + + def __init__(self, base_url: str, service_token: str, timeout: float = 120.0) -> None: + self._base_url = base_url.rstrip("/") + self._headers = {"Authorization": f"Bearer {service_token}"} if service_token else {} + self._timeout = timeout + + async def stream_chat(self, request: ShenshiChatRequest) -> AsyncIterator[ShenshiDelta]: + payload = request.model_dump(mode="json") + assert payload.get("stream") is True # 数据面当前只有流式形态 + async with httpx.AsyncClient(trust_env=False, timeout=self._timeout) as client: + async with client.stream( + "POST", + f"{self._base_url}/v1/chat/completions", + json=payload, + headers=self._headers, + ) as response: + if response.status_code >= 400: + body = (await response.aread()).decode("utf-8", errors="replace") + code = f"HJ-2{max(response.status_code - 400, 0):03d}" + try: + detail = json.loads(body) + code = detail.get("error", {}).get("code", code) + body = detail.get("error", {}).get("message", body) + except json.JSONDecodeError: + pass + raise ShenshiError(code, body) + async for line in response.aiter_lines(): + if not line.startswith("data:"): + continue + data = line[len("data:") :].strip() + if data == "[DONE]": + return + if data: + yield ShenshiDelta.model_validate_json(data) + + +def build_shenshi_client(base_url: str, service_token: str) -> ShenshiClient: + """按配置装配客户端:未配置神识地址时降级为回声桩(M0 骨架形态)。""" + if base_url: + return HttpShenshiClient(base_url, service_token) + return EchoShenshi() diff --git a/backend/app/shenshi_client/types.py b/backend/app/shenshi_client/types.py new file mode 100644 index 0000000..890e6ff --- /dev/null +++ b/backend/app/shenshi_client/types.py @@ -0,0 +1,63 @@ +"""神识客户端边界类型:灵台 → 神识的版本化 IR。 + +对应 docs/architecture/shenshi-gateway.md §5:灵台只发送内部 IR,不感知 Provider 方言。 +""" + +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field + +Role = Literal["system", "user", "assistant", "tool"] + + +class ShenshiMessage(BaseModel): + """IR 消息。tool 消息须携带 tool_call_id,与 tool_calls 配对不拆散。""" + + model_config = ConfigDict(extra="forbid") + + role: Role + content: str + tool_call_id: str | None = None + name: str | None = None + + +class ShenshiChatRequest(BaseModel): + """版本化模型请求(IR 入站)。 + + 逻辑模型与能力槽位解耦:默认按 ``slot`` 解析,``model`` 为人物覆写或镜树临时选择。 + """ + + model_config = ConfigDict(extra="forbid") + + ir_version: int = 1 + slot: str = "chat" + model: str | None = None + messages: list[ShenshiMessage] + run_id: str + trace_id: str | None = None + character_id: str | None = None + temperature: float | None = None + max_tokens: int | None = None + stream: bool = True + + +class ShenshiDelta(BaseModel): + """神识标准化流的增量事件(与 Provider 方言无关)。""" + + model_config = ConfigDict(extra="forbid") + + delta: str = "" + finish_reason: str | None = None + usage_prompt_tokens: int | None = None + usage_completion_tokens: int | None = None + + +class ShenshiError(Exception): + """神识侧错误:携带契约错误码(HJ-2xxx,见契约词典 §15)。""" + + def __init__(self, code: str, message: str) -> None: + super().__init__(message) + self.code = code + self.message = message diff --git a/backend/tests/test_kernel.py b/backend/tests/test_kernel.py new file mode 100644 index 0000000..42f9b69 --- /dev/null +++ b/backend/tests/test_kernel.py @@ -0,0 +1,166 @@ +"""Kernel M0 骨架测试。 + +覆盖 rewrite-plan M0 验收的核心面: +- 事件序列完整且有序(run.queued → run.started → … → run.finished); +- 命台席位:同人物第二个前台 Run 排队,其 run.started 晚于第一个的 run.finished; +- 瀑布否决:llm.pre_request 否决 → safety.ruled + run.interrupted(HJ-1301),无 llm.delta; +- 不变量断言:流重组不一致即 InvariantViolation; +- SSE 端到端:POST /runs → GET /runs/{id}/stream 读到 run.finished 终态。 +""" + +from __future__ import annotations + +import asyncio +from collections.abc import AsyncGenerator, AsyncIterator + +import httpx +import pytest +from httpx import ASGITransport, AsyncClient + +from app.db.session import AsyncSessionLocal +from app.kernel import Kernel, KernelEventLog +from app.kernel.invariants import InvariantViolation, assert_stream_reconstruction +from app.modules.runtime.events import Event +from app.runtime.bus import Veto +from app.services.event_store import DbEventStore +from app.shenshi_client import EchoShenshi, ShenshiDelta + + +class SlowEchoShenshi(EchoShenshi): + """带延迟的回声桩:让命台排队与 SSE 流时序可观测。""" + + delay: float = 0.15 + + async def stream_chat(self, request) -> AsyncIterator[ShenshiDelta]: # noqa: ANN001 + async for delta in super().stream_chat(request): + await asyncio.sleep(self.delay) + yield delta + + +@pytest.fixture +async def kernel_runtime(monkeypatch: pytest.MonkeyPatch) -> AsyncGenerator[Kernel, None]: + """手工装配 Kernel 到 app.state(测试不经 lifespan,与既有测试做法一致)。""" + from app.main import app + + event_store = DbEventStore(AsyncSessionLocal) + kernel = Kernel( + shenshi=SlowEchoShenshi(), + session_factory=AsyncSessionLocal, + event_log=KernelEventLog(event_store), + timeout_seconds=10.0, + ) + app.state.kernel = kernel + yield kernel + app.state.kernel = None + + +@pytest.fixture +async def kernel_client( + kernel_runtime: Kernel, auth_headers: dict[str, str] +) -> AsyncGenerator[AsyncClient, None]: + from app.main import app + + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test", headers=auth_headers) as c: + yield c + + +async def _wait_status(client: AsyncClient, run_id: str, target: set[str]) -> dict: + for _ in range(500): + resp = await client.get(f"/api/v1/runs/{run_id}") + body = resp.json() + if body["status"] in target: + return body + await asyncio.sleep(0.02) + raise AssertionError(f"Run 未到达终态:{run_id}") + + +async def _events(client: AsyncClient, run_id: str) -> list[dict]: + resp = await client.get(f"/api/v1/runs/{run_id}/events") + assert resp.status_code == 200 + return resp.json() + + +async def test_placeholder_run_event_sequence(kernel_client: AsyncClient) -> None: + created = await kernel_client.post("/api/v1/runs", json={"prompt": "你好"}) + assert created.status_code == 200 + run_id = created.json()["id"] + final = await _wait_status(kernel_client, run_id, {"completed"}) + assert final["origin"] == "kernel" + + types = [e["type"] for e in await _events(kernel_client, run_id)] + assert types[0] == "run.queued" + assert "run.started" in types + order = {t: types.index(t) for t in ["run.started", "context.projected", "llm.requested", "llm.completed", "result.accepted", "run.finished"]} + assert order["run.started"] < order["context.projected"] < order["llm.requested"] + assert order["llm.completed"] < order["result.accepted"] < order["run.finished"] + assert types[-1] == "run.finished" + assert types.count("llm.delta") >= 1 + + # "模型可见即可重建":llm.requested 事件携带完整 IR 消息 + requested = next(e for e in await _events(kernel_client, run_id) if e["type"] == "llm.requested") + messages = requested["payload"]["request"]["messages"] + assert messages[-1]["content"] == "你好" + + +async def test_seat_serializes_foreground(kernel_client: AsyncClient) -> None: + first = (await kernel_client.post("/api/v1/runs", json={"prompt": "第一个"})).json() + second = (await kernel_client.post("/api/v1/runs", json={"prompt": "第二个"})).json() + await _wait_status(kernel_client, first["id"], {"completed"}) + await _wait_status(kernel_client, second["id"], {"completed"}) + + def interval(events: list[dict]) -> tuple[str, str]: + started = next(e["created_at"] for e in events if e["type"] == "run.started") + finished = next(e["created_at"] for e in events if e["type"] == "run.finished") + return started, finished + + start1, finish1 = interval(await _events(kernel_client, first["id"])) + start2, finish2 = interval(await _events(kernel_client, second["id"])) + # 同人物前台串行:两个执行区间不相交(谁先谁后由调度决定) + late_start, early_finish = max(start1, start2), min(finish1, finish2) + assert late_start >= early_finish + + +async def test_veto_interrupts_run( + kernel_runtime: Kernel, kernel_client: AsyncClient +) -> None: + async def veto_llm(node: str, context) -> None: # noqa: ANN001, ARG001 + raise Veto("测试否决:禁止本次模型调用", rule="test_rule") + + kernel_runtime.bus.on("llm.pre_request", veto_llm) + try: + created = await kernel_client.post("/api/v1/runs", json={"prompt": "被否决"}) + run_id = created.json()["id"] + final = await _wait_status(kernel_client, run_id, {"interrupted"}) + assert final["error_code"] == "HJ-1301" + types = [e["type"] for e in await _events(kernel_client, run_id)] + assert "safety.ruled" in types + assert "llm.delta" not in types + assert types[-1] == "run.interrupted" + finally: + kernel_runtime.bus._listeners.pop("llm.pre_request", None) # noqa: SLF001 + + +def test_stream_reconstruction_invariant() -> None: + def ev(delta: str) -> Event: + return Event(type="llm.delta", source="kernel", payload={"delta": delta}) + + assert_stream_reconstruction([ev("你"), ev("好")], "你好") + with pytest.raises(InvariantViolation): + assert_stream_reconstruction([ev("你"), ev("好")], "你好吗") + + +async def test_sse_stream_end_to_end(kernel_client: AsyncClient) -> None: + created = await kernel_client.post("/api/v1/runs", json={"prompt": "流式验证"}) + run_id = created.json()["id"] + seen_types: list[str] = [] + async with kernel_client.stream("GET", f"/api/v1/runs/{run_id}/stream") as response: + assert response.status_code == 200 + async for line in response.aiter_lines(): + if line.startswith("event:"): + seen_types.append(line[len("event:") :].strip()) + if line.startswith("data:") and '"run.finished"' in line: + break + assert "run.started" in seen_types + assert "llm.delta" in seen_types + assert "run.finished" in seen_types