"""ORM rows for the independent enterprise-research workbench.""" from __future__ import annotations from datetime import UTC, datetime from sqlalchemy import Index, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column from deerflow.persistence.base import Base from deerflow.persistence.types import BeijingDateTime, PortableJSON, PortableLongText class EnterpriseResearchTaskRow(Base): """A user-owned research task plus its bounded WeKnora evidence snapshot.""" __tablename__ = "enterprise_research_tasks" __table_args__ = (Index("ix_enterprise_research_tasks_owner_updated", "user_id", "updated_at"),) id: Mapped[str] = mapped_column(String(64), primary_key=True) user_id: Mapped[str] = mapped_column(String(128), nullable=False, index=True) subject: Mapped[str] = mapped_column(String(256), nullable=False) focus: Mapped[str] = mapped_column(Text, nullable=False, default="", server_default="") template_id: Mapped[str] = mapped_column(String(64), nullable=False) knowledge_base_ids: Mapped[list[str]] = mapped_column(PortableJSON(), nullable=False, default=list) queries: Mapped[list[str]] = mapped_column(PortableJSON(), nullable=False, default=list) # Nullable keeps upgrades safe: the generic schema reconciler can add this # field to a phase-one table without an unsafe MySQL TEXT default. selected_source_ids: Mapped[list[str] | None] = mapped_column(PortableJSON(), nullable=True, default=list) # A WeKnora result can contain a long document chunk. MySQL TEXT is only # 64 KiB, so evidence uses LONGTEXT through PortableLongText. sources_json: Mapped[str] = mapped_column(PortableLongText(), nullable=False, default="[]", server_default="[]") plan_markdown: Mapped[str | None] = mapped_column(PortableLongText(), nullable=True) report_markdown: Mapped[str | None] = mapped_column(PortableLongText(), nullable=True) report_summary: Mapped[str | None] = mapped_column(PortableLongText(), nullable=True) report_model_name: Mapped[str | None] = mapped_column(String(256), nullable=True) active_report_job_id: Mapped[str | None] = mapped_column(String(64), nullable=True) status: Mapped[str] = mapped_column(String(32), nullable=False, default="draft", server_default="draft") warning: Mapped[str | None] = mapped_column(PortableLongText(), nullable=True) created_at: Mapped[datetime] = mapped_column(BeijingDateTime(), nullable=False, default=lambda: datetime.now(UTC)) updated_at: Mapped[datetime] = mapped_column( BeijingDateTime(), nullable=False, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC) ) class EnterpriseResearchReportJobRow(Base): """A report job whose plan/evidence are frozen when the user starts it.""" __tablename__ = "enterprise_research_report_jobs" __table_args__ = ( Index("ix_enterprise_research_report_jobs_task_owner_updated", "task_id", "user_id", "updated_at"), UniqueConstraint("active_dedupe_key", name="uq_enterprise_research_report_jobs_active"), ) id: Mapped[str] = mapped_column(String(64), primary_key=True) task_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True) user_id: Mapped[str] = mapped_column(String(128), nullable=False, index=True) status: Mapped[str] = mapped_column(String(32), nullable=False, default="queued", server_default="queued") phase: Mapped[str] = mapped_column(String(32), nullable=False, default="queued", server_default="queued") model_name: Mapped[str | None] = mapped_column(String(256), nullable=True) # Present only for queued/running rows. NULL terminal rows keep unlimited # history while this key rejects cross-process duplicate starts. active_dedupe_key: Mapped[str | None] = mapped_column(String(256), nullable=True) action: Mapped[str | None] = mapped_column(String(32), nullable=True, default="initial") instruction: Mapped[str | None] = mapped_column(PortableLongText(), nullable=True) style: Mapped[str | None] = mapped_column(String(32), nullable=True) target_length: Mapped[int | None] = mapped_column(nullable=True) plan_snapshot: Mapped[str] = mapped_column(PortableLongText(), nullable=False) sources_json: Mapped[str] = mapped_column(PortableLongText(), nullable=False) original_report: Mapped[str | None] = mapped_column(PortableLongText(), nullable=True) report_markdown: Mapped[str] = mapped_column(PortableLongText(), nullable=False, default="", server_default="") report_summary: Mapped[str | None] = mapped_column(PortableLongText(), nullable=True) error_message: Mapped[str | None] = mapped_column(PortableLongText(), nullable=True) created_at: Mapped[datetime] = mapped_column(BeijingDateTime(), nullable=False, default=lambda: datetime.now(UTC)) updated_at: Mapped[datetime] = mapped_column( BeijingDateTime(), nullable=False, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC) )