feat(models): 组织、用户、角色、客户、会话与消息模型
按组织边界定义数据关系,补齐仓储层与通用工具,迁移脚本可重复执行。
This commit is contained in:
1 parent
2aee6286d6
commit
5e0615f36d
126 files changed
+17436
No files matched your search
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
Copyright 2024-2026 ChatterMate
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import enum
|
||||
|
||||
from sqlalchemy import JSON, Boolean, Column, DateTime, Float, ForeignKey, Index, Integer, String
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.sql import expression, func
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class FAQJobType(str, enum.Enum):
|
||||
# Generate from every knowledge source in the org (the "Generate" button).
|
||||
GENERATE_ALL = "generate_all"
|
||||
# Generate from one newly processed source (the auto-generation hook).
|
||||
GENERATE_SOURCE = "generate_source"
|
||||
# Extract Q&A pairs from an external FAQ page URL (migration).
|
||||
IMPORT_URL = "import_url"
|
||||
# Crawl an external help-center index and import linked articles as-is
|
||||
# (HTML→Markdown, no LLM).
|
||||
IMPORT_ARTICLES = "import_articles"
|
||||
# Extract Q&A pairs from an uploaded PDF.
|
||||
IMPORT_PDF = "import_pdf"
|
||||
|
||||
|
||||
class FAQJobStatus(str, enum.Enum):
|
||||
PENDING = "pending"
|
||||
PROCESSING = "processing"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class FAQJobStage(str, enum.Enum):
|
||||
NOT_STARTED = "not_started"
|
||||
ANALYZING_SOURCES = "analyzing_sources"
|
||||
EXTRACTING = "extracting"
|
||||
DRAFTING = "drafting"
|
||||
GROUPING = "grouping"
|
||||
COMPLETED = "completed"
|
||||
|
||||
|
||||
# Statuses that count as "still running" for enqueue guards and UI polling —
|
||||
# derived so a future status defaults to "active" rather than silently
|
||||
# escaping the duplicate-job guard.
|
||||
TERMINAL_FAQ_JOB_STATUSES = {FAQJobStatus.COMPLETED, FAQJobStatus.FAILED}
|
||||
ACTIVE_FAQ_JOB_STATUSES = [s.value for s in FAQJobStatus if s not in TERMINAL_FAQ_JOB_STATUSES]
|
||||
|
||||
|
||||
class FAQGenerationJob(Base):
|
||||
"""Async FAQ generation/import job, processed by the FAQ worker.
|
||||
|
||||
Mirrors the KnowledgeQueue shape (string status/stage + float progress) so
|
||||
the established worker and frontend polling patterns carry over.
|
||||
"""
|
||||
__tablename__ = "faq_generation_jobs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
organization_id = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("organizations.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
# Nullable: auto-generation jobs may be enqueued without an acting user.
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
||||
job_type = Column(String, nullable=False, default=FAQJobType.GENERATE_ALL)
|
||||
# GENERATE_SOURCE: the knowledge source to read. NULL for GENERATE_ALL/
|
||||
# IMPORT_URL. SET NULL (not CASCADE) so deleting a source mid-run doesn't
|
||||
# yank the job row out from under the worker and the UI's polling.
|
||||
knowledge_id = Column(Integer, ForeignKey("knowledge.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
# GENERATE_ALL: optional explicit narrowing to specific knowledge sources
|
||||
# (overrides the default skip-already-generated behaviour).
|
||||
knowledge_ids = Column(JSON, nullable=True)
|
||||
# IMPORT_URL: the external page to extract from. IMPORT_PDF: the stored
|
||||
# upload path/URL the worker reads the file from.
|
||||
source_url = Column(String, nullable=True)
|
||||
# IMPORT_PDF: the original client filename, used for source_label.
|
||||
source_file_name = Column(String(255), nullable=True)
|
||||
# IMPORT_ARTICLES: keep every imported article at the URL path it already
|
||||
# has on the source help center (FAQ.url_path), so migrating in doesn't
|
||||
# move — and cost the org — the URLs it already ranks for.
|
||||
preserve_source_urls = Column(
|
||||
Boolean, nullable=False, default=False, server_default=expression.false()
|
||||
)
|
||||
# LLM calls attempted by this job (retries included) — the usage unit for
|
||||
# hosted-model metering. `metered` is stamped at enqueue time from the
|
||||
# org's AI config so the period sum never depends on later config changes.
|
||||
llm_calls = Column(Integer, nullable=False, default=0, server_default="0")
|
||||
metered = Column(Boolean, nullable=False, default=True, server_default=expression.true())
|
||||
status = Column(String, nullable=False, default=FAQJobStatus.PENDING)
|
||||
stage = Column(String, nullable=False, default=FAQJobStage.NOT_STARTED)
|
||||
progress_percentage = Column(Float, nullable=False, default=0.0)
|
||||
faqs_created = Column(Integer, nullable=False, default=0)
|
||||
error = Column(String, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
__table_args__ = (
|
||||
# Partial index keeping the worker's poll O(active rows).
|
||||
Index(
|
||||
"ix_faq_generation_jobs_active",
|
||||
"status",
|
||||
postgresql_where=(status.in_(ACTIVE_FAQ_JOB_STATUSES)),
|
||||
),
|
||||
# NOTE: production also has uq_faq_generation_jobs_one_active — a
|
||||
# UNIQUE partial index on (organization_id, job_type,
|
||||
# COALESCE(knowledge_id, -1)) WHERE status is active, closing the
|
||||
# enqueue check-then-insert race. It lives only in the migration:
|
||||
# declaring it here would create it WITHOUT the partial WHERE on
|
||||
# sqlite (tests), wrongly blocking re-enqueues after completion.
|
||||
)
|
||||
Reference in new issue
Block a user