fix(dev): idempotent Alembic chain for squashed 0001 + clearer dev scripts
- Make migrations 0002–0008 skip schema changes already applied when 0001_initial creates current ORM (rename segments column, timeline FK, memoir phase flags, drop content_tsv, eval_* tables). - development.sh / internal-eval.sh: surface Alembic stderr, warn on docker-style DB hosts, TCP port checks without lsof, verify Uvicorn listens before claiming started.
This commit is contained in:
@@ -8,6 +8,7 @@ Revises: 0001_initial
|
|||||||
|
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
from alembic import op
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "0002_segments_user_input_text"
|
revision: str = "0002_segments_user_input_text"
|
||||||
@@ -16,9 +17,19 @@ branch_labels: Union[str, Sequence[str], None] = None
|
|||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _column_names(table_name: str) -> set[str]:
|
||||||
|
bind = op.get_bind()
|
||||||
|
inspector = sa.inspect(bind)
|
||||||
|
return {column["name"] for column in inspector.get_columns(table_name)}
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
op.execute("ALTER TABLE segments RENAME COLUMN transcript_text TO user_input_text")
|
columns = _column_names("segments")
|
||||||
|
if "transcript_text" in columns and "user_input_text" not in columns:
|
||||||
|
op.execute("ALTER TABLE segments RENAME COLUMN transcript_text TO user_input_text")
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
op.execute("ALTER TABLE segments RENAME COLUMN user_input_text TO transcript_text")
|
columns = _column_names("segments")
|
||||||
|
if "user_input_text" in columns and "transcript_text" not in columns:
|
||||||
|
op.execute("ALTER TABLE segments RENAME COLUMN user_input_text TO transcript_text")
|
||||||
|
|||||||
@@ -15,31 +15,67 @@ branch_labels: Union[str, Sequence[str], None] = None
|
|||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _column_names(table_name: str) -> set[str]:
|
||||||
|
bind = op.get_bind()
|
||||||
|
inspector = sa.inspect(bind)
|
||||||
|
return {column["name"] for column in inspector.get_columns(table_name)}
|
||||||
|
|
||||||
|
|
||||||
|
def _index_names(table_name: str) -> set[str]:
|
||||||
|
bind = op.get_bind()
|
||||||
|
inspector = sa.inspect(bind)
|
||||||
|
return {index["name"] for index in inspector.get_indexes(table_name)}
|
||||||
|
|
||||||
|
|
||||||
|
def _foreign_key_names(table_name: str) -> set[str]:
|
||||||
|
bind = op.get_bind()
|
||||||
|
inspector = sa.inspect(bind)
|
||||||
|
return {
|
||||||
|
fk["name"] for fk in inspector.get_foreign_keys(table_name) if fk.get("name")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
op.add_column(
|
columns = _column_names("timeline_events")
|
||||||
"timeline_events",
|
if "memory_source_id" not in columns:
|
||||||
sa.Column("memory_source_id", sa.String(), nullable=True),
|
op.add_column(
|
||||||
)
|
"timeline_events",
|
||||||
op.create_index(
|
sa.Column("memory_source_id", sa.String(), nullable=True),
|
||||||
"ix_timeline_events_memory_source_id",
|
)
|
||||||
"timeline_events",
|
|
||||||
["memory_source_id"],
|
indexes = _index_names("timeline_events")
|
||||||
)
|
if "ix_timeline_events_memory_source_id" not in indexes:
|
||||||
op.create_foreign_key(
|
op.create_index(
|
||||||
"fk_timeline_events_memory_source_id_memory_sources",
|
"ix_timeline_events_memory_source_id",
|
||||||
"timeline_events",
|
"timeline_events",
|
||||||
"memory_sources",
|
["memory_source_id"],
|
||||||
["memory_source_id"],
|
)
|
||||||
["id"],
|
|
||||||
ondelete="SET NULL",
|
foreign_keys = _foreign_key_names("timeline_events")
|
||||||
)
|
if "fk_timeline_events_memory_source_id_memory_sources" not in foreign_keys:
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_timeline_events_memory_source_id_memory_sources",
|
||||||
|
"timeline_events",
|
||||||
|
"memory_sources",
|
||||||
|
["memory_source_id"],
|
||||||
|
["id"],
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
op.drop_constraint(
|
foreign_keys = _foreign_key_names("timeline_events")
|
||||||
"fk_timeline_events_memory_source_id_memory_sources",
|
if "fk_timeline_events_memory_source_id_memory_sources" in foreign_keys:
|
||||||
"timeline_events",
|
op.drop_constraint(
|
||||||
type_="foreignkey",
|
"fk_timeline_events_memory_source_id_memory_sources",
|
||||||
)
|
"timeline_events",
|
||||||
op.drop_index("ix_timeline_events_memory_source_id", table_name="timeline_events")
|
type_="foreignkey",
|
||||||
op.drop_column("timeline_events", "memory_source_id")
|
)
|
||||||
|
|
||||||
|
indexes = _index_names("timeline_events")
|
||||||
|
if "ix_timeline_events_memory_source_id" in indexes:
|
||||||
|
op.drop_index("ix_timeline_events_memory_source_id", table_name="timeline_events")
|
||||||
|
|
||||||
|
columns = _column_names("timeline_events")
|
||||||
|
if "memory_source_id" in columns:
|
||||||
|
op.drop_column("timeline_events", "memory_source_id")
|
||||||
|
|||||||
@@ -16,27 +16,39 @@ branch_labels: Union[str, Sequence[str], None] = None
|
|||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _column_names(table_name: str) -> set[str]:
|
||||||
|
bind = op.get_bind()
|
||||||
|
inspector = sa.inspect(bind)
|
||||||
|
return {column["name"] for column in inspector.get_columns(table_name)}
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
op.add_column(
|
columns = _column_names("segments")
|
||||||
"segments",
|
if "narrated" not in columns:
|
||||||
sa.Column(
|
op.add_column(
|
||||||
"narrated",
|
"segments",
|
||||||
sa.Boolean(),
|
sa.Column(
|
||||||
nullable=False,
|
"narrated",
|
||||||
server_default=sa.text("false"),
|
sa.Boolean(),
|
||||||
),
|
nullable=False,
|
||||||
)
|
server_default=sa.text("false"),
|
||||||
op.add_column(
|
),
|
||||||
"segments",
|
)
|
||||||
sa.Column(
|
if "skip_narrative" not in columns:
|
||||||
"skip_narrative",
|
op.add_column(
|
||||||
sa.Boolean(),
|
"segments",
|
||||||
nullable=False,
|
sa.Column(
|
||||||
server_default=sa.text("false"),
|
"skip_narrative",
|
||||||
),
|
sa.Boolean(),
|
||||||
)
|
nullable=False,
|
||||||
|
server_default=sa.text("false"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
op.drop_column("segments", "skip_narrative")
|
columns = _column_names("segments")
|
||||||
op.drop_column("segments", "narrated")
|
if "skip_narrative" in columns:
|
||||||
|
op.drop_column("segments", "skip_narrative")
|
||||||
|
if "narrated" in columns:
|
||||||
|
op.drop_column("segments", "narrated")
|
||||||
|
|||||||
@@ -17,12 +17,22 @@ branch_labels: Union[str, Sequence[str], None] = None
|
|||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _column_names(table_name: str) -> set[str]:
|
||||||
|
bind = op.get_bind()
|
||||||
|
inspector = sa.inspect(bind)
|
||||||
|
return {column["name"] for column in inspector.get_columns(table_name)}
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
op.drop_column("memory_chunks", "content_tsv")
|
columns = _column_names("memory_chunks")
|
||||||
|
if "content_tsv" in columns:
|
||||||
|
op.drop_column("memory_chunks", "content_tsv")
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
op.add_column(
|
columns = _column_names("memory_chunks")
|
||||||
"memory_chunks",
|
if "content_tsv" not in columns:
|
||||||
sa.Column("content_tsv", postgresql.TSVECTOR(), nullable=True),
|
op.add_column(
|
||||||
)
|
"memory_chunks",
|
||||||
|
sa.Column("content_tsv", postgresql.TSVECTOR(), nullable=True),
|
||||||
|
)
|
||||||
|
|||||||
@@ -17,214 +17,246 @@ branch_labels: Union[str, Sequence[str], None] = None
|
|||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _has_table(table_name: str) -> bool:
|
||||||
|
bind = op.get_bind()
|
||||||
|
inspector = sa.inspect(bind)
|
||||||
|
return inspector.has_table(table_name)
|
||||||
|
|
||||||
|
|
||||||
|
def _index_names(table_name: str) -> set[str]:
|
||||||
|
bind = op.get_bind()
|
||||||
|
inspector = sa.inspect(bind)
|
||||||
|
return {index["name"] for index in inspector.get_indexes(table_name)}
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
op.create_table(
|
if not _has_table("eval_regression_sets"):
|
||||||
"eval_regression_sets",
|
op.create_table(
|
||||||
sa.Column("id", sa.String(), nullable=False),
|
"eval_regression_sets",
|
||||||
sa.Column("name", sa.String(), nullable=False),
|
sa.Column("id", sa.String(), nullable=False),
|
||||||
sa.Column("description", sa.Text(), nullable=True),
|
sa.Column("name", sa.String(), nullable=False),
|
||||||
sa.Column(
|
sa.Column("description", sa.Text(), nullable=True),
|
||||||
"created_at",
|
sa.Column(
|
||||||
sa.DateTime(timezone=True),
|
"created_at",
|
||||||
server_default=sa.text("now()"),
|
sa.DateTime(timezone=True),
|
||||||
nullable=False,
|
server_default=sa.text("now()"),
|
||||||
),
|
nullable=False,
|
||||||
sa.PrimaryKeyConstraint("id"),
|
),
|
||||||
)
|
sa.PrimaryKeyConstraint("id"),
|
||||||
op.create_table(
|
)
|
||||||
"eval_versions",
|
if not _has_table("eval_versions"):
|
||||||
sa.Column("id", sa.String(), nullable=False),
|
op.create_table(
|
||||||
sa.Column("name", sa.String(), nullable=False),
|
"eval_versions",
|
||||||
sa.Column("runner_kind", sa.String(), nullable=False),
|
sa.Column("id", sa.String(), nullable=False),
|
||||||
sa.Column(
|
sa.Column("name", sa.String(), nullable=False),
|
||||||
"config_json", postgresql.JSONB(astext_type=sa.Text()), nullable=True
|
sa.Column("runner_kind", sa.String(), nullable=False),
|
||||||
),
|
sa.Column(
|
||||||
sa.Column(
|
"config_json", postgresql.JSONB(astext_type=sa.Text()), nullable=True
|
||||||
"created_at",
|
),
|
||||||
sa.DateTime(timezone=True),
|
sa.Column(
|
||||||
server_default=sa.text("now()"),
|
"created_at",
|
||||||
nullable=False,
|
sa.DateTime(timezone=True),
|
||||||
),
|
server_default=sa.text("now()"),
|
||||||
sa.PrimaryKeyConstraint("id"),
|
nullable=False,
|
||||||
)
|
),
|
||||||
op.create_table(
|
sa.PrimaryKeyConstraint("id"),
|
||||||
"eval_cases",
|
)
|
||||||
sa.Column("id", sa.String(), nullable=False),
|
if not _has_table("eval_cases"):
|
||||||
sa.Column("regression_set_id", sa.String(), nullable=False),
|
op.create_table(
|
||||||
sa.Column("source_conversation_id", sa.String(), nullable=True),
|
"eval_cases",
|
||||||
sa.Column("source_user_id", sa.String(), nullable=True),
|
sa.Column("id", sa.String(), nullable=False),
|
||||||
sa.Column("title", sa.String(), nullable=True),
|
sa.Column("regression_set_id", sa.String(), nullable=False),
|
||||||
sa.Column(
|
sa.Column("source_conversation_id", sa.String(), nullable=True),
|
||||||
"user_utterances",
|
sa.Column("source_user_id", sa.String(), nullable=True),
|
||||||
postgresql.JSONB(astext_type=sa.Text()),
|
sa.Column("title", sa.String(), nullable=True),
|
||||||
nullable=False,
|
sa.Column(
|
||||||
),
|
"user_utterances",
|
||||||
sa.Column("reference_memoir_markdown", sa.Text(), nullable=True),
|
postgresql.JSONB(astext_type=sa.Text()),
|
||||||
sa.Column(
|
nullable=False,
|
||||||
"is_protected",
|
),
|
||||||
sa.Boolean(),
|
sa.Column("reference_memoir_markdown", sa.Text(), nullable=True),
|
||||||
server_default=sa.text("false"),
|
sa.Column(
|
||||||
nullable=False,
|
"is_protected",
|
||||||
),
|
sa.Boolean(),
|
||||||
sa.Column("meta", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
|
server_default=sa.text("false"),
|
||||||
sa.Column(
|
nullable=False,
|
||||||
"created_at",
|
),
|
||||||
sa.DateTime(timezone=True),
|
sa.Column("meta", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
|
||||||
server_default=sa.text("now()"),
|
sa.Column(
|
||||||
nullable=False,
|
"created_at",
|
||||||
),
|
sa.DateTime(timezone=True),
|
||||||
sa.ForeignKeyConstraint(
|
server_default=sa.text("now()"),
|
||||||
["regression_set_id"],
|
nullable=False,
|
||||||
["eval_regression_sets.id"],
|
),
|
||||||
ondelete="CASCADE",
|
sa.ForeignKeyConstraint(
|
||||||
),
|
["regression_set_id"],
|
||||||
sa.PrimaryKeyConstraint("id"),
|
["eval_regression_sets.id"],
|
||||||
)
|
ondelete="CASCADE",
|
||||||
op.create_index(
|
),
|
||||||
"ix_eval_cases_source_conversation_id",
|
sa.PrimaryKeyConstraint("id"),
|
||||||
"eval_cases",
|
)
|
||||||
["source_conversation_id"],
|
indexes = _index_names("eval_cases")
|
||||||
unique=False,
|
if "ix_eval_cases_source_conversation_id" not in indexes:
|
||||||
)
|
op.create_index(
|
||||||
op.create_index(
|
"ix_eval_cases_source_conversation_id",
|
||||||
"ix_eval_cases_source_user_id",
|
"eval_cases",
|
||||||
"eval_cases",
|
["source_conversation_id"],
|
||||||
["source_user_id"],
|
unique=False,
|
||||||
unique=False,
|
)
|
||||||
)
|
if "ix_eval_cases_source_user_id" not in indexes:
|
||||||
op.create_table(
|
op.create_index(
|
||||||
"eval_experiments",
|
"ix_eval_cases_source_user_id",
|
||||||
sa.Column("id", sa.String(), nullable=False),
|
"eval_cases",
|
||||||
sa.Column("name", sa.String(), nullable=False),
|
["source_user_id"],
|
||||||
sa.Column("regression_set_id", sa.String(), nullable=False),
|
unique=False,
|
||||||
sa.Column("baseline_version_id", sa.String(), nullable=False),
|
)
|
||||||
sa.Column("candidate_version_id", sa.String(), nullable=False),
|
if not _has_table("eval_experiments"):
|
||||||
sa.Column("rubric_pack", sa.String(), nullable=False),
|
op.create_table(
|
||||||
sa.Column(
|
"eval_experiments",
|
||||||
"composite_weights_json",
|
sa.Column("id", sa.String(), nullable=False),
|
||||||
postgresql.JSONB(astext_type=sa.Text()),
|
sa.Column("name", sa.String(), nullable=False),
|
||||||
nullable=True,
|
sa.Column("regression_set_id", sa.String(), nullable=False),
|
||||||
),
|
sa.Column("baseline_version_id", sa.String(), nullable=False),
|
||||||
sa.Column("status", sa.String(), nullable=False),
|
sa.Column("candidate_version_id", sa.String(), nullable=False),
|
||||||
sa.Column("error_message", sa.Text(), nullable=True),
|
sa.Column("rubric_pack", sa.String(), nullable=False),
|
||||||
sa.Column(
|
sa.Column(
|
||||||
"created_at",
|
"composite_weights_json",
|
||||||
sa.DateTime(timezone=True),
|
postgresql.JSONB(astext_type=sa.Text()),
|
||||||
server_default=sa.text("now()"),
|
nullable=True,
|
||||||
nullable=False,
|
),
|
||||||
),
|
sa.Column("status", sa.String(), nullable=False),
|
||||||
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
|
sa.Column("error_message", sa.Text(), nullable=True),
|
||||||
sa.ForeignKeyConstraint(
|
sa.Column(
|
||||||
["baseline_version_id"],
|
"created_at",
|
||||||
["eval_versions.id"],
|
sa.DateTime(timezone=True),
|
||||||
),
|
server_default=sa.text("now()"),
|
||||||
sa.ForeignKeyConstraint(
|
nullable=False,
|
||||||
["candidate_version_id"],
|
),
|
||||||
["eval_versions.id"],
|
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
),
|
sa.ForeignKeyConstraint(
|
||||||
sa.ForeignKeyConstraint(
|
["baseline_version_id"],
|
||||||
["regression_set_id"],
|
["eval_versions.id"],
|
||||||
["eval_regression_sets.id"],
|
),
|
||||||
),
|
sa.ForeignKeyConstraint(
|
||||||
sa.PrimaryKeyConstraint("id"),
|
["candidate_version_id"],
|
||||||
)
|
["eval_versions.id"],
|
||||||
op.create_table(
|
),
|
||||||
"eval_runs",
|
sa.ForeignKeyConstraint(
|
||||||
sa.Column("id", sa.String(), nullable=False),
|
["regression_set_id"],
|
||||||
sa.Column("experiment_id", sa.String(), nullable=False),
|
["eval_regression_sets.id"],
|
||||||
sa.Column("case_id", sa.String(), nullable=False),
|
),
|
||||||
sa.Column("side", sa.String(), nullable=False),
|
sa.PrimaryKeyConstraint("id"),
|
||||||
sa.Column("status", sa.String(), nullable=False),
|
)
|
||||||
sa.Column("error_message", sa.Text(), nullable=True),
|
if not _has_table("eval_runs"):
|
||||||
sa.Column("memoir_markdown", sa.Text(), nullable=True),
|
op.create_table(
|
||||||
sa.Column("conversation_score_total", sa.Float(), nullable=True),
|
"eval_runs",
|
||||||
sa.Column("memoir_score_total", sa.Float(), nullable=True),
|
sa.Column("id", sa.String(), nullable=False),
|
||||||
sa.Column("composite_score", sa.Float(), nullable=True),
|
sa.Column("experiment_id", sa.String(), nullable=False),
|
||||||
sa.Column(
|
sa.Column("case_id", sa.String(), nullable=False),
|
||||||
"judge_bundle_json",
|
sa.Column("side", sa.String(), nullable=False),
|
||||||
postgresql.JSONB(astext_type=sa.Text()),
|
sa.Column("status", sa.String(), nullable=False),
|
||||||
nullable=True,
|
sa.Column("error_message", sa.Text(), nullable=True),
|
||||||
),
|
sa.Column("memoir_markdown", sa.Text(), nullable=True),
|
||||||
sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
|
sa.Column("conversation_score_total", sa.Float(), nullable=True),
|
||||||
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
|
sa.Column("memoir_score_total", sa.Float(), nullable=True),
|
||||||
sa.ForeignKeyConstraint(
|
sa.Column("composite_score", sa.Float(), nullable=True),
|
||||||
["case_id"],
|
sa.Column(
|
||||||
["eval_cases.id"],
|
"judge_bundle_json",
|
||||||
),
|
postgresql.JSONB(astext_type=sa.Text()),
|
||||||
sa.ForeignKeyConstraint(
|
nullable=True,
|
||||||
["experiment_id"],
|
),
|
||||||
["eval_experiments.id"],
|
sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
ondelete="CASCADE",
|
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
),
|
sa.ForeignKeyConstraint(
|
||||||
sa.PrimaryKeyConstraint("id"),
|
["case_id"],
|
||||||
sa.UniqueConstraint(
|
["eval_cases.id"],
|
||||||
"experiment_id",
|
),
|
||||||
"case_id",
|
sa.ForeignKeyConstraint(
|
||||||
"side",
|
["experiment_id"],
|
||||||
name="uq_eval_run_experiment_case_side",
|
["eval_experiments.id"],
|
||||||
),
|
ondelete="CASCADE",
|
||||||
)
|
),
|
||||||
op.create_table(
|
sa.PrimaryKeyConstraint("id"),
|
||||||
"eval_run_turns",
|
sa.UniqueConstraint(
|
||||||
sa.Column("id", sa.String(), nullable=False),
|
"experiment_id",
|
||||||
sa.Column("run_id", sa.String(), nullable=False),
|
"case_id",
|
||||||
sa.Column("turn_index", sa.Integer(), nullable=False),
|
"side",
|
||||||
sa.Column("user_utterance", sa.Text(), nullable=False),
|
name="uq_eval_run_experiment_case_side",
|
||||||
sa.Column("assistant_reply", sa.Text(), nullable=True),
|
),
|
||||||
sa.Column("duration_ms", sa.Integer(), nullable=True),
|
)
|
||||||
sa.Column(
|
if not _has_table("eval_run_turns"):
|
||||||
"judge_scores_json",
|
op.create_table(
|
||||||
postgresql.JSONB(astext_type=sa.Text()),
|
"eval_run_turns",
|
||||||
nullable=True,
|
sa.Column("id", sa.String(), nullable=False),
|
||||||
),
|
sa.Column("run_id", sa.String(), nullable=False),
|
||||||
sa.Column("judge_rationale", sa.Text(), nullable=True),
|
sa.Column("turn_index", sa.Integer(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(
|
sa.Column("user_utterance", sa.Text(), nullable=False),
|
||||||
["run_id"],
|
sa.Column("assistant_reply", sa.Text(), nullable=True),
|
||||||
["eval_runs.id"],
|
sa.Column("duration_ms", sa.Integer(), nullable=True),
|
||||||
ondelete="CASCADE",
|
sa.Column(
|
||||||
),
|
"judge_scores_json",
|
||||||
sa.PrimaryKeyConstraint("id"),
|
postgresql.JSONB(astext_type=sa.Text()),
|
||||||
sa.UniqueConstraint("run_id", "turn_index", name="uq_eval_run_turn_index"),
|
nullable=True,
|
||||||
)
|
),
|
||||||
op.create_table(
|
sa.Column("judge_rationale", sa.Text(), nullable=True),
|
||||||
"eval_gate_verdicts",
|
sa.ForeignKeyConstraint(
|
||||||
sa.Column("id", sa.String(), nullable=False),
|
["run_id"],
|
||||||
sa.Column("experiment_id", sa.String(), nullable=False),
|
["eval_runs.id"],
|
||||||
sa.Column("passed", sa.Boolean(), nullable=False),
|
ondelete="CASCADE",
|
||||||
sa.Column("mean_composite_delta", sa.Float(), nullable=True),
|
),
|
||||||
sa.Column(
|
sa.PrimaryKeyConstraint("id"),
|
||||||
"protected_regressions_json",
|
sa.UniqueConstraint("run_id", "turn_index", name="uq_eval_run_turn_index"),
|
||||||
postgresql.JSONB(astext_type=sa.Text()),
|
)
|
||||||
nullable=True,
|
if not _has_table("eval_gate_verdicts"):
|
||||||
),
|
op.create_table(
|
||||||
sa.Column(
|
"eval_gate_verdicts",
|
||||||
"details_json",
|
sa.Column("id", sa.String(), nullable=False),
|
||||||
postgresql.JSONB(astext_type=sa.Text()),
|
sa.Column("experiment_id", sa.String(), nullable=False),
|
||||||
nullable=True,
|
sa.Column("passed", sa.Boolean(), nullable=False),
|
||||||
),
|
sa.Column("mean_composite_delta", sa.Float(), nullable=True),
|
||||||
sa.Column(
|
sa.Column(
|
||||||
"computed_at",
|
"protected_regressions_json",
|
||||||
sa.DateTime(timezone=True),
|
postgresql.JSONB(astext_type=sa.Text()),
|
||||||
server_default=sa.text("now()"),
|
nullable=True,
|
||||||
nullable=False,
|
),
|
||||||
),
|
sa.Column(
|
||||||
sa.ForeignKeyConstraint(
|
"details_json",
|
||||||
["experiment_id"],
|
postgresql.JSONB(astext_type=sa.Text()),
|
||||||
["eval_experiments.id"],
|
nullable=True,
|
||||||
ondelete="CASCADE",
|
),
|
||||||
),
|
sa.Column(
|
||||||
sa.PrimaryKeyConstraint("id"),
|
"computed_at",
|
||||||
sa.UniqueConstraint("experiment_id"),
|
sa.DateTime(timezone=True),
|
||||||
)
|
server_default=sa.text("now()"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["experiment_id"],
|
||||||
|
["eval_experiments.id"],
|
||||||
|
ondelete="CASCADE",
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("experiment_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
op.drop_table("eval_gate_verdicts")
|
if _has_table("eval_gate_verdicts"):
|
||||||
op.drop_table("eval_run_turns")
|
op.drop_table("eval_gate_verdicts")
|
||||||
op.drop_table("eval_runs")
|
if _has_table("eval_run_turns"):
|
||||||
op.drop_table("eval_experiments")
|
op.drop_table("eval_run_turns")
|
||||||
op.drop_index("ix_eval_cases_source_user_id", table_name="eval_cases")
|
if _has_table("eval_runs"):
|
||||||
op.drop_index("ix_eval_cases_source_conversation_id", table_name="eval_cases")
|
op.drop_table("eval_runs")
|
||||||
op.drop_table("eval_cases")
|
if _has_table("eval_experiments"):
|
||||||
op.drop_table("eval_versions")
|
op.drop_table("eval_experiments")
|
||||||
op.drop_table("eval_regression_sets")
|
if _has_table("eval_cases"):
|
||||||
|
indexes = _index_names("eval_cases")
|
||||||
|
if "ix_eval_cases_source_user_id" in indexes:
|
||||||
|
op.drop_index("ix_eval_cases_source_user_id", table_name="eval_cases")
|
||||||
|
if "ix_eval_cases_source_conversation_id" in indexes:
|
||||||
|
op.drop_index("ix_eval_cases_source_conversation_id", table_name="eval_cases")
|
||||||
|
op.drop_table("eval_cases")
|
||||||
|
if _has_table("eval_versions"):
|
||||||
|
op.drop_table("eval_versions")
|
||||||
|
if _has_table("eval_regression_sets"):
|
||||||
|
op.drop_table("eval_regression_sets")
|
||||||
|
|||||||
@@ -163,6 +163,122 @@ wait_postgres_ready() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_effective_database_url() {
|
||||||
|
if [[ -n "${DATABASE_URL:-}" ]]; then
|
||||||
|
printf '%s\n' "${DATABASE_URL}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "${ROOT_DIR}/.env" ]]; then
|
||||||
|
local line
|
||||||
|
line="$(sed -n 's/^DATABASE_URL=//p' "${ROOT_DIR}/.env" | sed -n '1p')"
|
||||||
|
line="${line%\"}"
|
||||||
|
line="${line#\"}"
|
||||||
|
line="${line%\'}"
|
||||||
|
line="${line#\'}"
|
||||||
|
if [[ -n "${line}" ]]; then
|
||||||
|
printf '%s\n' "${line}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
warn_database_url_host_pitfall() {
|
||||||
|
local database_url
|
||||||
|
local host
|
||||||
|
|
||||||
|
if ! database_url="$(get_effective_database_url)"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${database_url}" =~ @([^:/?#]+) ]]; then
|
||||||
|
host="${BASH_REMATCH[1]}"
|
||||||
|
case "${host}" in
|
||||||
|
postgres|db|postgres-dev|postgresql)
|
||||||
|
print_warn "检测到 DATABASE_URL 主机为 ${host};在宿主机执行 Alembic/uvicorn 时通常应使用 localhost"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_alembic_failure_hint() {
|
||||||
|
local log_file="$1"
|
||||||
|
local log_output
|
||||||
|
|
||||||
|
log_output="$(sed -n '1,200p' "${log_file}")"
|
||||||
|
if [[ "${log_output}" == *'could not translate host name "postgres"'* ]] || [[ "${log_output}" == *"Name or service not known"* ]]; then
|
||||||
|
print_warn "看起来 DATABASE_URL 指向了容器内主机名;在宿主机运行时请改用 localhost:5432"
|
||||||
|
elif [[ "${log_output}" == *"Connection refused"* ]] || [[ "${log_output}" == *"could not connect to server"* ]]; then
|
||||||
|
print_warn "PostgreSQL 连接被拒绝;请确认容器已启动且 DATABASE_URL 与 docker-compose.dev.yml 暴露端口一致"
|
||||||
|
elif [[ "${log_output}" == *"password authentication failed"* ]]; then
|
||||||
|
print_warn "PostgreSQL 用户名或密码不匹配;请核对 .env.development 中的 DATABASE_URL"
|
||||||
|
elif [[ "${log_output}" == *"No such file or directory"* ]] || [[ "${log_output}" == *"can't open file"* ]]; then
|
||||||
|
print_warn "Alembic 依赖的文件或工作目录可能不正确;请确认在 api/ 目录运行脚本"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
is_port_listening() {
|
||||||
|
local port="$1"
|
||||||
|
|
||||||
|
if command -v lsof >/dev/null 2>&1; then
|
||||||
|
lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -x "${PYTHON_BIN}" ]]; then
|
||||||
|
"${PYTHON_BIN}" - "${port}" <<'PY' >/dev/null 2>&1
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sock = socket.socket()
|
||||||
|
sock.settimeout(0.2)
|
||||||
|
try:
|
||||||
|
sock.connect(("127.0.0.1", int(sys.argv[1])))
|
||||||
|
except OSError:
|
||||||
|
raise SystemExit(1)
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
raise SystemExit(0)
|
||||||
|
PY
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_tcp_listener() {
|
||||||
|
local pid="$1"
|
||||||
|
local port="$2"
|
||||||
|
local timeout="${3:-8}"
|
||||||
|
local waited=0
|
||||||
|
|
||||||
|
while (( waited < timeout )); do
|
||||||
|
if is_port_listening "${port}"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if ! is_pid_alive "${pid}"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
waited=$((waited + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_background_process_alive() {
|
||||||
|
local name="$1"
|
||||||
|
local pid="$2"
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
if ! is_pid_alive "${pid}"; then
|
||||||
|
print_err "${name} 启动后立即退出,请查看上方日志"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
ensure_venv() {
|
ensure_venv() {
|
||||||
print_header "检查 Python 虚拟环境"
|
print_header "检查 Python 虚拟环境"
|
||||||
|
|
||||||
@@ -199,16 +315,25 @@ check_env_file() {
|
|||||||
print_warn "未找到 .env,应用可能因缺少配置启动失败"
|
print_warn "未找到 .env,应用可能因缺少配置启动失败"
|
||||||
else
|
else
|
||||||
print_ok "检测到 .env"
|
print_ok "检测到 .env"
|
||||||
|
warn_database_url_host_pitfall
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
run_migrations() {
|
run_migrations() {
|
||||||
print_header "执行数据库迁移"
|
print_header "执行数据库迁移"
|
||||||
cd "${ROOT_DIR}"
|
cd "${ROOT_DIR}"
|
||||||
if uv run alembic upgrade head 2>/dev/null; then
|
local log_file
|
||||||
|
log_file="$(mktemp -t life-echo-alembic.XXXXXX.log)"
|
||||||
|
|
||||||
|
if uv run alembic upgrade head >"${log_file}" 2>&1; then
|
||||||
print_ok "Alembic 迁移已就绪"
|
print_ok "Alembic 迁移已就绪"
|
||||||
|
rm -f "${log_file}"
|
||||||
else
|
else
|
||||||
print_warn "Alembic 迁移失败(可能数据库未启动或 DATABASE_URL 未配置),应用启动可能失败"
|
print_warn "Alembic 迁移失败(可能数据库未启动或 DATABASE_URL 未配置),应用启动可能失败"
|
||||||
|
print_alembic_failure_hint "${log_file}"
|
||||||
|
print_warn "Alembic 输出(最近 40 行):"
|
||||||
|
tail -n 40 "${log_file}"
|
||||||
|
rm -f "${log_file}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,12 +341,10 @@ start_services() {
|
|||||||
print_header "启动 FastAPI 和 Celery"
|
print_header "启动 FastAPI 和 Celery"
|
||||||
cd "${ROOT_DIR}"
|
cd "${ROOT_DIR}"
|
||||||
|
|
||||||
if command -v lsof >/dev/null 2>&1; then
|
if is_port_listening "${API_PORT}"; then
|
||||||
if lsof -nP -iTCP:"${API_PORT}" -sTCP:LISTEN >/dev/null 2>&1; then
|
print_err "端口 ${API_PORT} 已被占用,无法启动新的 Uvicorn。"
|
||||||
print_err "端口 ${API_PORT} 已被占用,无法启动新的 Uvicorn。"
|
print_err "请先结束占用进程,例如: lsof -nP -iTCP:${API_PORT} -sTCP:LISTEN"
|
||||||
print_err "请先结束占用进程,例如: lsof -nP -iTCP:${API_PORT} -sTCP:LISTEN"
|
exit 1
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 迁移由 main.py 在启动时执行;排除 alembic 目录与 alembic.ini,避免编辑迁移时触发整进程重载
|
# 迁移由 main.py 在启动时执行;排除 alembic 目录与 alembic.ini,避免编辑迁移时触发整进程重载
|
||||||
@@ -230,10 +353,30 @@ start_services() {
|
|||||||
--reload-exclude 'alembic.ini' \
|
--reload-exclude 'alembic.ini' \
|
||||||
--host "${API_HOST}" --port "${API_PORT}" &
|
--host "${API_HOST}" --port "${API_PORT}" &
|
||||||
API_PID=$!
|
API_PID=$!
|
||||||
print_ok "FastAPI 已启动 (PID: ${API_PID})"
|
local api_start_status=0
|
||||||
|
if wait_for_tcp_listener "${API_PID}" "${API_PORT}" 8; then
|
||||||
|
api_start_status=0
|
||||||
|
else
|
||||||
|
api_start_status=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${api_start_status}" in
|
||||||
|
0)
|
||||||
|
print_ok "FastAPI 已启动 (PID: ${API_PID})"
|
||||||
|
;;
|
||||||
|
1)
|
||||||
|
print_err "FastAPI 启动失败,进程已退出;请查看上方 Uvicorn 日志"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_err "FastAPI 进程仍存活,但端口 ${API_PORT} 未在预期时间内开始监听"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
"${CELERY_BIN}" -A app.tasks.celery_app worker --loglevel=info --pool="${CELERY_POOL}" &
|
"${CELERY_BIN}" -A app.tasks.celery_app worker --loglevel=info --pool="${CELERY_POOL}" &
|
||||||
CELERY_PID=$!
|
CELERY_PID=$!
|
||||||
|
ensure_background_process_alive "Celery" "${CELERY_PID}"
|
||||||
print_ok "Celery 已启动 (PID: ${CELERY_PID})"
|
print_ok "Celery 已启动 (PID: ${CELERY_PID})"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
|
|||||||
@@ -194,6 +194,122 @@ wait_postgres_ready() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_effective_database_url() {
|
||||||
|
if [[ -n "${DATABASE_URL:-}" ]]; then
|
||||||
|
printf '%s\n' "${DATABASE_URL}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "${ROOT_DIR}/.env" ]]; then
|
||||||
|
local line
|
||||||
|
line="$(sed -n 's/^DATABASE_URL=//p' "${ROOT_DIR}/.env" | sed -n '1p')"
|
||||||
|
line="${line%\"}"
|
||||||
|
line="${line#\"}"
|
||||||
|
line="${line%\'}"
|
||||||
|
line="${line#\'}"
|
||||||
|
if [[ -n "${line}" ]]; then
|
||||||
|
printf '%s\n' "${line}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
warn_database_url_host_pitfall() {
|
||||||
|
local database_url
|
||||||
|
local host
|
||||||
|
|
||||||
|
if ! database_url="$(get_effective_database_url)"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${database_url}" =~ @([^:/?#]+) ]]; then
|
||||||
|
host="${BASH_REMATCH[1]}"
|
||||||
|
case "${host}" in
|
||||||
|
postgres|db|postgres-dev|postgresql)
|
||||||
|
print_warn "检测到 DATABASE_URL 主机为 ${host};在宿主机执行 Alembic/uvicorn 时通常应使用 localhost"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_alembic_failure_hint() {
|
||||||
|
local log_file="$1"
|
||||||
|
local log_output
|
||||||
|
|
||||||
|
log_output="$(sed -n '1,200p' "${log_file}")"
|
||||||
|
if [[ "${log_output}" == *'could not translate host name "postgres"'* ]] || [[ "${log_output}" == *"Name or service not known"* ]]; then
|
||||||
|
print_warn "看起来 DATABASE_URL 指向了容器内主机名;在宿主机运行时请改用 localhost:5432"
|
||||||
|
elif [[ "${log_output}" == *"Connection refused"* ]] || [[ "${log_output}" == *"could not connect to server"* ]]; then
|
||||||
|
print_warn "PostgreSQL 连接被拒绝;请确认容器已启动且 DATABASE_URL 与 docker-compose.dev.yml 暴露端口一致"
|
||||||
|
elif [[ "${log_output}" == *"password authentication failed"* ]]; then
|
||||||
|
print_warn "PostgreSQL 用户名或密码不匹配;请核对 .env.development 中的 DATABASE_URL"
|
||||||
|
elif [[ "${log_output}" == *"No such file or directory"* ]] || [[ "${log_output}" == *"can't open file"* ]]; then
|
||||||
|
print_warn "Alembic 依赖的文件或工作目录可能不正确;请确认在 api/ 目录运行脚本"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
is_port_listening() {
|
||||||
|
local port="$1"
|
||||||
|
|
||||||
|
if command -v lsof >/dev/null 2>&1; then
|
||||||
|
lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -x "${VENV_DIR}/bin/python" ]]; then
|
||||||
|
"${VENV_DIR}/bin/python" - "${port}" <<'PY' >/dev/null 2>&1
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sock = socket.socket()
|
||||||
|
sock.settimeout(0.2)
|
||||||
|
try:
|
||||||
|
sock.connect(("127.0.0.1", int(sys.argv[1])))
|
||||||
|
except OSError:
|
||||||
|
raise SystemExit(1)
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
raise SystemExit(0)
|
||||||
|
PY
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_tcp_listener() {
|
||||||
|
local pid="$1"
|
||||||
|
local port="$2"
|
||||||
|
local timeout="${3:-8}"
|
||||||
|
local waited=0
|
||||||
|
|
||||||
|
while (( waited < timeout )); do
|
||||||
|
if is_port_listening "${port}"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if ! is_pid_alive "${pid}"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
waited=$((waited + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_background_process_alive() {
|
||||||
|
local name="$1"
|
||||||
|
local pid="$2"
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
if ! is_pid_alive "${pid}"; then
|
||||||
|
print_err "${name} 启动后立即退出,请查看上方日志"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
ensure_venv() {
|
ensure_venv() {
|
||||||
print_header "检查 Python 虚拟环境"
|
print_header "检查 Python 虚拟环境"
|
||||||
|
|
||||||
@@ -243,16 +359,25 @@ check_env_file() {
|
|||||||
print_warn "未找到 .env,应用可能因缺少配置启动失败"
|
print_warn "未找到 .env,应用可能因缺少配置启动失败"
|
||||||
else
|
else
|
||||||
print_ok "检测到 .env"
|
print_ok "检测到 .env"
|
||||||
|
warn_database_url_host_pitfall
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
run_migrations() {
|
run_migrations() {
|
||||||
print_header "执行数据库迁移"
|
print_header "执行数据库迁移"
|
||||||
cd "${ROOT_DIR}"
|
cd "${ROOT_DIR}"
|
||||||
if uv run alembic upgrade head 2>/dev/null; then
|
local log_file
|
||||||
|
log_file="$(mktemp -t life-echo-alembic.XXXXXX.log)"
|
||||||
|
|
||||||
|
if uv run alembic upgrade head >"${log_file}" 2>&1; then
|
||||||
print_ok "Alembic 迁移已就绪"
|
print_ok "Alembic 迁移已就绪"
|
||||||
|
rm -f "${log_file}"
|
||||||
else
|
else
|
||||||
print_warn "Alembic 迁移失败(可能数据库未启动或 DATABASE_URL 未配置),应用启动可能失败"
|
print_warn "Alembic 迁移失败(可能数据库未启动或 DATABASE_URL 未配置),应用启动可能失败"
|
||||||
|
print_alembic_failure_hint "${log_file}"
|
||||||
|
print_warn "Alembic 输出(最近 40 行):"
|
||||||
|
tail -n 40 "${log_file}"
|
||||||
|
rm -f "${log_file}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,12 +422,10 @@ start_services() {
|
|||||||
print_header "启动 Internal Eval API 与 Celery"
|
print_header "启动 Internal Eval API 与 Celery"
|
||||||
cd "${ROOT_DIR}"
|
cd "${ROOT_DIR}"
|
||||||
|
|
||||||
if command -v lsof >/dev/null 2>&1; then
|
if is_port_listening "${INTERNAL_EVAL_PORT}"; then
|
||||||
if lsof -nP -iTCP:"${INTERNAL_EVAL_PORT}" -sTCP:LISTEN >/dev/null 2>&1; then
|
print_err "端口 ${INTERNAL_EVAL_PORT} 已被占用,无法启动内部评测 Uvicorn。"
|
||||||
print_err "端口 ${INTERNAL_EVAL_PORT} 已被占用,无法启动内部评测 Uvicorn。"
|
print_err "请先结束占用进程,或设置 INTERNAL_EVAL_PORT 为其他端口"
|
||||||
print_err "请先结束占用进程,或设置 INTERNAL_EVAL_PORT 为其他端口"
|
exit 1
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 与主开发脚本一致:评审/生产 LLM 等从 .env 读取;文档默认关闭,本地可 export INTERNAL_EVAL_ENABLE_DOCS=1
|
# 与主开发脚本一致:评审/生产 LLM 等从 .env 读取;文档默认关闭,本地可 export INTERNAL_EVAL_ENABLE_DOCS=1
|
||||||
@@ -311,11 +434,31 @@ start_services() {
|
|||||||
--reload-exclude 'alembic.ini' \
|
--reload-exclude 'alembic.ini' \
|
||||||
--host "${INTERNAL_EVAL_HOST}" --port "${INTERNAL_EVAL_PORT}" &
|
--host "${INTERNAL_EVAL_HOST}" --port "${INTERNAL_EVAL_PORT}" &
|
||||||
API_PID=$!
|
API_PID=$!
|
||||||
print_ok "Internal Eval API 已启动 (PID: ${API_PID})"
|
local api_start_status=0
|
||||||
|
if wait_for_tcp_listener "${API_PID}" "${INTERNAL_EVAL_PORT}" 8; then
|
||||||
|
api_start_status=0
|
||||||
|
else
|
||||||
|
api_start_status=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${api_start_status}" in
|
||||||
|
0)
|
||||||
|
print_ok "Internal Eval API 已启动 (PID: ${API_PID})"
|
||||||
|
;;
|
||||||
|
1)
|
||||||
|
print_err "Internal Eval API 启动失败,进程已退出;请查看上方 Uvicorn 日志"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_err "Internal Eval API 进程仍存活,但端口 ${INTERNAL_EVAL_PORT} 未在预期时间内开始监听"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
if [[ "${SKIP_CELERY}" != "1" ]]; then
|
if [[ "${SKIP_CELERY}" != "1" ]]; then
|
||||||
"${CELERY_BIN}" -A app.tasks.celery_app worker --loglevel=info --pool="${CELERY_POOL}" &
|
"${CELERY_BIN}" -A app.tasks.celery_app worker --loglevel=info --pool="${CELERY_POOL}" &
|
||||||
CELERY_PID=$!
|
CELERY_PID=$!
|
||||||
|
ensure_background_process_alive "Celery" "${CELERY_PID}"
|
||||||
print_ok "Celery 已启动 (PID: ${CELERY_PID})"
|
print_ok "Celery 已启动 (PID: ${CELERY_PID})"
|
||||||
else
|
else
|
||||||
print_warn "已跳过 Celery (SKIP_CELERY=1);实验 run 接口需要 worker 才能执行"
|
print_warn "已跳过 Celery (SKIP_CELERY=1);实验 run 接口需要 worker 才能执行"
|
||||||
|
|||||||
Reference in New Issue
Block a user