M0:灵台 Kernel 骨架(事件溯源/命台席位/瀑布总线/插件注册)与神识客户端边界,新增 POST /runs 占位闭环与测试,Alembic 基线合并为单一起点
This commit is contained in:
1 parent
99e55fa0a8
commit
7ef3a15061
23 files changed
+2014
-1399
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -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")
|
|
||||||
@@ -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 ###
|
|
||||||
@@ -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 ###
|
|
||||||
@@ -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 ###
|
||||||
@@ -17,6 +17,7 @@ from app.api.v1.endpoints import (
|
|||||||
goals,
|
goals,
|
||||||
health,
|
health,
|
||||||
imports,
|
imports,
|
||||||
|
kernel_runs,
|
||||||
lorebooks,
|
lorebooks,
|
||||||
mcp,
|
mcp,
|
||||||
memory,
|
memory,
|
||||||
@@ -38,6 +39,7 @@ api_router.include_router(characters.router)
|
|||||||
api_router.include_router(character_import.router)
|
api_router.include_router(character_import.router)
|
||||||
api_router.include_router(chat.router)
|
api_router.include_router(chat.router)
|
||||||
api_router.include_router(runs.router)
|
api_router.include_router(runs.router)
|
||||||
|
api_router.include_router(kernel_runs.router)
|
||||||
api_router.include_router(memory.router)
|
api_router.include_router(memory.router)
|
||||||
api_router.include_router(lorebooks.router)
|
api_router.include_router(lorebooks.router)
|
||||||
api_router.include_router(personality.router)
|
api_router.include_router(personality.router)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -14,6 +14,11 @@ class RuntimeConfig(BaseSettings):
|
|||||||
sse_poll_interval_seconds: float = 0.01 # SSE 轮询间隔(秒,10ms:前端逐段平滑增量)
|
sse_poll_interval_seconds: float = 0.01 # SSE 轮询间隔(秒,10ms:前端逐段平滑增量)
|
||||||
sse_heartbeat_seconds: float = 15.0 # SSE 心跳间隔(秒)
|
sse_heartbeat_seconds: float = 15.0 # SSE 心跳间隔(秒)
|
||||||
run_timeout_seconds: int = 120 # 运行超时时间(秒)
|
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 循环最大轮数:模型每轮仍带工具调用则继续,超限截断返回(见 agent.runtime.DEFAULT_MAX_ITERATIONS)
|
||||||
agent_max_iterations: int = 8
|
agent_max_iterations: int = 8
|
||||||
# 单次高风险工具确认挂起超时(秒):须小于 run_timeout_seconds,超时按拒绝处理;
|
# 单次高风险工具确认挂起超时(秒):须小于 run_timeout_seconds,超时按拒绝处理;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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)} 字符",
|
||||||
|
)
|
||||||
@@ -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()
|
||||||
@@ -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)
|
||||||
@@ -86,6 +86,18 @@ async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
|
|||||||
# 事件流与历史查询不依赖沙箱,始终可用。
|
# 事件流与历史查询不依赖沙箱,始终可用。
|
||||||
_app.state.event_store = DbEventStore(AsyncSessionLocal)
|
_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 子进程生命周期管理) ──────────────
|
# ── MCP 进程管理器(stdio 子进程生命周期管理) ──────────────
|
||||||
from app.integrations.mcp.process_manager import (
|
from app.integrations.mcp.process_manager import (
|
||||||
ProcessManager,
|
ProcessManager,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
]
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -14,7 +14,15 @@ from collections import defaultdict
|
|||||||
|
|
||||||
from app.schemas.runtime import EventResponse
|
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:
|
class RunEventBus:
|
||||||
|
|||||||
@@ -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",
|
||||||
|
]
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in new issue
Block a user