ai-robot-core/ai-service/app/models/entities.py

749 lines
27 KiB
Python

"""
Memory layer entities for AI Service.
[AC-AISVC-13] SQLModel entities for chat sessions and messages with tenant isolation.
"""
import uuid
from datetime import datetime
from enum import Enum
from typing import Any
from sqlalchemy import JSON, Column
from sqlmodel import Field, Index, SQLModel
class ChatSession(SQLModel, table=True):
"""
[AC-AISVC-13] Chat session entity with tenant isolation.
Primary key: (tenant_id, session_id) composite unique constraint.
"""
__tablename__ = "chat_sessions"
__table_args__ = (
Index("ix_chat_sessions_tenant_session", "tenant_id", "session_id", unique=True),
Index("ix_chat_sessions_tenant_id", "tenant_id"),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
tenant_id: str = Field(..., description="Tenant ID for multi-tenant isolation", index=True)
session_id: str = Field(..., description="Session ID for conversation tracking")
channel_type: str | None = Field(default=None, description="Channel type: wechat, douyin, jd")
metadata_: dict[str, Any] | None = Field(
default=None,
sa_column=Column("metadata", JSON, nullable=True),
description="Session metadata"
)
created_at: datetime = Field(default_factory=datetime.utcnow, description="Session creation time")
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
class ChatMessage(SQLModel, table=True):
"""
[AC-AISVC-13] Chat message entity with tenant isolation.
Messages are scoped by (tenant_id, session_id) for multi-tenant security.
"""
__tablename__ = "chat_messages"
__table_args__ = (
Index("ix_chat_messages_tenant_session", "tenant_id", "session_id"),
Index("ix_chat_messages_tenant_session_created", "tenant_id", "session_id", "created_at"),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
tenant_id: str = Field(..., description="Tenant ID for multi-tenant isolation", index=True)
session_id: str = Field(..., description="Session ID for conversation tracking", index=True)
role: str = Field(..., description="Message role: user or assistant")
content: str = Field(..., description="Message content")
prompt_tokens: int | None = Field(default=None, description="Number of prompt tokens used")
completion_tokens: int | None = Field(default=None, description="Number of completion tokens used")
total_tokens: int | None = Field(default=None, description="Total tokens used")
latency_ms: int | None = Field(default=None, description="Response latency in milliseconds")
first_token_ms: int | None = Field(default=None, description="Time to first token in milliseconds (for streaming)")
is_error: bool = Field(default=False, description="Whether this message is an error response")
error_message: str | None = Field(default=None, description="Error message if any")
created_at: datetime = Field(default_factory=datetime.utcnow, description="Message creation time")
class ChatSessionCreate(SQLModel):
"""Schema for creating a new chat session."""
tenant_id: str
session_id: str
channel_type: str | None = None
metadata_: dict[str, Any] | None = None
class ChatMessageCreate(SQLModel):
"""Schema for creating a new chat message."""
tenant_id: str
session_id: str
role: str
content: str
class DocumentStatus(str, Enum):
PENDING = "pending"
PROCESSING = "processing"
COMPLETED = "completed"
FAILED = "failed"
class IndexJobStatus(str, Enum):
PENDING = "pending"
PROCESSING = "processing"
COMPLETED = "completed"
FAILED = "failed"
class SessionStatus(str, Enum):
ACTIVE = "active"
CLOSED = "closed"
EXPIRED = "expired"
class Tenant(SQLModel, table=True):
"""
[AC-AISVC-10] Tenant entity for storing tenant information.
Tenant ID format: name@ash@year (e.g., szmp@ash@2026)
"""
__tablename__ = "tenants"
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
tenant_id: str = Field(..., description="Full tenant ID (format: name@ash@year)", unique=True, index=True)
name: str = Field(..., description="Tenant display name (first part of tenant_id)")
year: str = Field(..., description="Year part from tenant_id")
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation time")
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
class KBType(str, Enum):
PRODUCT = "product"
FAQ = "faq"
SCRIPT = "script"
POLICY = "policy"
GENERAL = "general"
class KnowledgeBase(SQLModel, table=True):
"""
[AC-ASA-01, AC-AISVC-59] Knowledge base entity with tenant isolation.
[v0.6.0] Extended with kb_type, priority, is_enabled, doc_count for multi-KB management.
"""
__tablename__ = "knowledge_bases"
__table_args__ = (
Index("ix_knowledge_bases_tenant_id", "tenant_id"),
Index("ix_knowledge_bases_tenant_kb_type", "tenant_id", "kb_type"),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
tenant_id: str = Field(..., description="Tenant ID for multi-tenant isolation", index=True)
name: str = Field(..., description="Knowledge base name")
kb_type: str = Field(
default=KBType.GENERAL.value,
description="Knowledge base type: product/faq/script/policy/general"
)
description: str | None = Field(default=None, description="Knowledge base description")
priority: int = Field(default=0, ge=0, description="Priority weight, higher value means higher priority")
is_enabled: bool = Field(default=True, description="Whether the knowledge base is enabled")
doc_count: int = Field(default=0, ge=0, description="Document count (cached statistic)")
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation time")
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
class Document(SQLModel, table=True):
"""
[AC-ASA-01, AC-ASA-08] Document entity with tenant isolation.
"""
__tablename__ = "documents"
__table_args__ = (
Index("ix_documents_tenant_kb", "tenant_id", "kb_id"),
Index("ix_documents_tenant_status", "tenant_id", "status"),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
tenant_id: str = Field(..., description="Tenant ID for multi-tenant isolation", index=True)
kb_id: str = Field(..., description="Knowledge base ID")
file_name: str = Field(..., description="Original file name")
file_path: str | None = Field(default=None, description="Storage path")
file_size: int | None = Field(default=None, description="File size in bytes")
file_type: str | None = Field(default=None, description="File MIME type")
status: str = Field(default=DocumentStatus.PENDING.value, description="Document status")
error_msg: str | None = Field(default=None, description="Error message if failed")
created_at: datetime = Field(default_factory=datetime.utcnow, description="Upload time")
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
class IndexJob(SQLModel, table=True):
"""
[AC-ASA-02] Index job entity for tracking document indexing progress.
"""
__tablename__ = "index_jobs"
__table_args__ = (
Index("ix_index_jobs_tenant_doc", "tenant_id", "doc_id"),
Index("ix_index_jobs_tenant_status", "tenant_id", "status"),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
tenant_id: str = Field(..., description="Tenant ID for multi-tenant isolation", index=True)
doc_id: uuid.UUID = Field(..., description="Document ID being indexed")
status: str = Field(default=IndexJobStatus.PENDING.value, description="Job status")
progress: int = Field(default=0, ge=0, le=100, description="Progress percentage")
error_msg: str | None = Field(default=None, description="Error message if failed")
created_at: datetime = Field(default_factory=datetime.utcnow, description="Job creation time")
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
class KnowledgeBaseCreate(SQLModel):
"""Schema for creating a new knowledge base."""
name: str
kb_type: str = KBType.GENERAL.value
description: str | None = None
priority: int = 0
class KnowledgeBaseUpdate(SQLModel):
"""Schema for updating a knowledge base."""
name: str | None = None
kb_type: str | None = None
description: str | None = None
priority: int | None = None
is_enabled: bool | None = None
class DocumentCreate(SQLModel):
"""Schema for creating a new document."""
tenant_id: str
kb_id: str
file_name: str
file_path: str | None = None
file_size: int | None = None
file_type: str | None = None
class ApiKey(SQLModel, table=True):
"""
[AC-AISVC-50] API Key entity for lightweight authentication.
Keys are loaded into memory on startup for fast validation.
"""
__tablename__ = "api_keys"
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
key: str = Field(..., description="API Key (unique)", unique=True, index=True)
name: str = Field(..., description="Key name/description for identification")
is_active: bool = Field(default=True, description="Whether the key is active")
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation time")
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
class ApiKeyCreate(SQLModel):
"""Schema for creating a new API key."""
key: str
name: str
is_active: bool = True
class TemplateVersionStatus(str, Enum):
DRAFT = "draft"
PUBLISHED = "published"
ARCHIVED = "archived"
class PromptTemplate(SQLModel, table=True):
"""
[AC-AISVC-51, AC-AISVC-52] Prompt template entity with tenant isolation.
Main table for storing template metadata.
"""
__tablename__ = "prompt_templates"
__table_args__ = (
Index("ix_prompt_templates_tenant_scene", "tenant_id", "scene"),
Index("ix_prompt_templates_tenant_id", "tenant_id"),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
tenant_id: str = Field(..., description="Tenant ID for multi-tenant isolation", index=True)
name: str = Field(..., description="Template name, e.g., 'Default Customer Service Persona'")
scene: str = Field(..., description="Scene tag: chat/rag_qa/greeting/farewell")
description: str | None = Field(default=None, description="Template description")
is_default: bool = Field(default=False, description="Whether this is the default template for the scene")
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation time")
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
class PromptTemplateVersion(SQLModel, table=True):
"""
[AC-AISVC-53] Prompt template version entity.
Stores versioned content with status management.
"""
__tablename__ = "prompt_template_versions"
__table_args__ = (
Index("ix_template_versions_template_status", "template_id", "status"),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
template_id: uuid.UUID = Field(
...,
description="Foreign key to prompt_templates.id",
foreign_key="prompt_templates.id",
index=True
)
version: int = Field(..., description="Version number (auto-incremented per template)")
status: str = Field(
default=TemplateVersionStatus.DRAFT.value,
description="Version status: draft/published/archived"
)
system_instruction: str = Field(
...,
description="System instruction content with {{variable}} placeholders"
)
variables: list[dict[str, Any]] | None = Field(
default=None,
sa_column=Column("variables", JSON, nullable=True),
description="Variable definitions, e.g., [{'name': 'persona_name', 'default': '小N'}]"
)
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation time")
class PromptTemplateCreate(SQLModel):
"""Schema for creating a new prompt template."""
name: str
scene: str
description: str | None = None
system_instruction: str
variables: list[dict[str, Any]] | None = None
is_default: bool = False
class PromptTemplateUpdate(SQLModel):
"""Schema for updating a prompt template."""
name: str | None = None
scene: str | None = None
description: str | None = None
system_instruction: str | None = None
variables: list[dict[str, Any]] | None = None
is_default: bool | None = None
class ResponseType(str, Enum):
"""[AC-AISVC-65] Response type for intent rules."""
FIXED = "fixed"
RAG = "rag"
FLOW = "flow"
TRANSFER = "transfer"
class IntentRule(SQLModel, table=True):
"""
[AC-AISVC-65] Intent rule entity with tenant isolation.
Supports keyword and regex matching for intent recognition.
"""
__tablename__ = "intent_rules"
__table_args__ = (
Index("ix_intent_rules_tenant_enabled_priority", "tenant_id", "is_enabled"),
Index("ix_intent_rules_tenant_id", "tenant_id"),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
tenant_id: str = Field(..., description="Tenant ID for multi-tenant isolation", index=True)
name: str = Field(..., description="Intent name, e.g., 'Return Intent'")
keywords: list[str] | None = Field(
default=None,
sa_column=Column("keywords", JSON, nullable=True),
description="Keyword list for matching, e.g., ['return', 'refund']"
)
patterns: list[str] | None = Field(
default=None,
sa_column=Column("patterns", JSON, nullable=True),
description="Regex pattern list for matching, e.g., ['return.*goods', 'how to return']"
)
priority: int = Field(default=0, description="Priority (higher value = higher priority)")
response_type: str = Field(..., description="Response type: fixed/rag/flow/transfer")
target_kb_ids: list[str] | None = Field(
default=None,
sa_column=Column("target_kb_ids", JSON, nullable=True),
description="Target knowledge base IDs for rag type"
)
flow_id: uuid.UUID | None = Field(default=None, description="Flow ID for flow type")
fixed_reply: str | None = Field(default=None, description="Fixed reply content for fixed type")
transfer_message: str | None = Field(default=None, description="Transfer message for transfer type")
is_enabled: bool = Field(default=True, description="Whether the rule is enabled")
hit_count: int = Field(default=0, description="Hit count for statistics")
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation time")
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
class IntentRuleCreate(SQLModel):
"""[AC-AISVC-65] Schema for creating a new intent rule."""
name: str
keywords: list[str] | None = None
patterns: list[str] | None = None
priority: int = 0
response_type: str
target_kb_ids: list[str] | None = None
flow_id: str | None = None
fixed_reply: str | None = None
transfer_message: str | None = None
class IntentRuleUpdate(SQLModel):
"""[AC-AISVC-67] Schema for updating an intent rule."""
name: str | None = None
keywords: list[str] | None = None
patterns: list[str] | None = None
priority: int | None = None
response_type: str | None = None
target_kb_ids: list[str] | None = None
flow_id: str | None = None
fixed_reply: str | None = None
transfer_message: str | None = None
is_enabled: bool | None = None
class IntentMatchResult:
"""
[AC-AISVC-69] Result of intent matching.
Contains the matched rule and match details.
"""
def __init__(
self,
rule: IntentRule,
match_type: str,
matched: str,
):
self.rule = rule
self.match_type = match_type
self.matched = matched
def to_dict(self) -> dict[str, Any]:
return {
"rule_id": str(self.rule.id),
"rule_name": self.rule.name,
"match_type": self.match_type,
"matched": self.matched,
"response_type": self.rule.response_type,
}
class ForbiddenWordCategory(str, Enum):
"""[AC-AISVC-78] Forbidden word category."""
COMPETITOR = "competitor"
SENSITIVE = "sensitive"
POLITICAL = "political"
CUSTOM = "custom"
class ForbiddenWordStrategy(str, Enum):
"""[AC-AISVC-78] Forbidden word replacement strategy."""
MASK = "mask"
REPLACE = "replace"
BLOCK = "block"
class ForbiddenWord(SQLModel, table=True):
"""
[AC-AISVC-78] Forbidden word entity with tenant isolation.
Supports mask/replace/block strategies for output filtering.
"""
__tablename__ = "forbidden_words"
__table_args__ = (
Index("ix_forbidden_words_tenant_enabled", "tenant_id", "is_enabled"),
Index("ix_forbidden_words_tenant_id", "tenant_id"),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
tenant_id: str = Field(..., description="Tenant ID for multi-tenant isolation", index=True)
word: str = Field(..., description="Forbidden word to detect")
category: str = Field(..., description="Category: competitor/sensitive/political/custom")
strategy: str = Field(..., description="Replacement strategy: mask/replace/block")
replacement: str | None = Field(default=None, description="Replacement text for 'replace' strategy")
fallback_reply: str | None = Field(default=None, description="Fallback reply for 'block' strategy")
is_enabled: bool = Field(default=True, description="Whether the word is enabled")
hit_count: int = Field(default=0, ge=0, description="Hit count for statistics")
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation time")
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
class ForbiddenWordCreate(SQLModel):
"""[AC-AISVC-78] Schema for creating a new forbidden word."""
word: str
category: str
strategy: str
replacement: str | None = None
fallback_reply: str | None = None
class ForbiddenWordUpdate(SQLModel):
"""[AC-AISVC-80] Schema for updating a forbidden word."""
word: str | None = None
category: str | None = None
strategy: str | None = None
replacement: str | None = None
fallback_reply: str | None = None
is_enabled: bool | None = None
class BehaviorRuleCategory(str, Enum):
"""[AC-AISVC-84] Behavior rule category."""
COMPLIANCE = "compliance"
TONE = "tone"
BOUNDARY = "boundary"
CUSTOM = "custom"
class BehaviorRule(SQLModel, table=True):
"""
[AC-AISVC-84] Behavior rule entity with tenant isolation.
These rules are injected into Prompt system instruction as LLM behavior constraints.
"""
__tablename__ = "behavior_rules"
__table_args__ = (
Index("ix_behavior_rules_tenant_enabled", "tenant_id", "is_enabled"),
Index("ix_behavior_rules_tenant_id", "tenant_id"),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
tenant_id: str = Field(..., description="Tenant ID for multi-tenant isolation", index=True)
rule_text: str = Field(
...,
description="Behavior constraint description, e.g., 'Do not promise specific compensation'"
)
category: str = Field(..., description="Category: compliance/tone/boundary/custom")
is_enabled: bool = Field(default=True, description="Whether the rule is enabled")
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation time")
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
class BehaviorRuleCreate(SQLModel):
"""[AC-AISVC-84] Schema for creating a new behavior rule."""
rule_text: str
category: str
class BehaviorRuleUpdate(SQLModel):
"""[AC-AISVC-85] Schema for updating a behavior rule."""
rule_text: str | None = None
category: str | None = None
is_enabled: bool | None = None
class GuardrailResult:
"""
[AC-AISVC-82] Result of guardrail filtering.
Contains filtered reply and trigger information.
"""
def __init__(
self,
reply: str,
blocked: bool = False,
triggered_words: list[str] | None = None,
triggered_categories: list[str] | None = None,
):
self.reply = reply
self.blocked = blocked
self.triggered_words = triggered_words or []
self.triggered_categories = triggered_categories or []
def to_dict(self) -> dict[str, Any]:
return {
"reply": self.reply,
"blocked": self.blocked,
"triggered_words": self.triggered_words,
"triggered_categories": self.triggered_categories,
"guardrail_triggered": len(self.triggered_words) > 0,
}
class InputScanResult:
"""
[AC-AISVC-83] Result of input scanning.
Contains flagged status and matched words (for logging only, no blocking).
"""
def __init__(
self,
flagged: bool = False,
matched_words: list[str] | None = None,
matched_categories: list[str] | None = None,
):
self.flagged = flagged
self.matched_words = matched_words or []
self.matched_categories = matched_categories or []
def to_dict(self) -> dict[str, Any]:
return {
"input_flagged": self.flagged,
"matched_words": self.matched_words,
"matched_categories": self.matched_categories,
}
class FlowInstanceStatus(str, Enum):
"""[AC-AISVC-74~AC-AISVC-77] Flow instance status."""
ACTIVE = "active"
COMPLETED = "completed"
TIMEOUT = "timeout"
CANCELLED = "cancelled"
class TimeoutAction(str, Enum):
"""[AC-AISVC-71] Timeout action for flow steps."""
REPEAT = "repeat"
SKIP = "skip"
TRANSFER = "transfer"
class ScriptFlow(SQLModel, table=True):
"""
[AC-AISVC-71] Script flow entity with tenant isolation.
Stores flow definition with steps in JSONB format.
"""
__tablename__ = "script_flows"
__table_args__ = (
Index("ix_script_flows_tenant_id", "tenant_id"),
Index("ix_script_flows_tenant_enabled", "tenant_id", "is_enabled"),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
tenant_id: str = Field(..., description="Tenant ID for multi-tenant isolation", index=True)
name: str = Field(..., description="Flow name")
description: str | None = Field(default=None, description="Flow description")
steps: list[dict[str, Any]] = Field(
default=[],
sa_column=Column("steps", JSON, nullable=False),
description="Flow steps list with step_no, content, wait_input, timeout_seconds"
)
is_enabled: bool = Field(default=True, description="Whether the flow is enabled")
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation time")
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
class FlowInstance(SQLModel, table=True):
"""
[AC-AISVC-74] Flow instance entity for runtime state.
Tracks active flow execution per session.
"""
__tablename__ = "flow_instances"
__table_args__ = (
Index("ix_flow_instances_tenant_session", "tenant_id", "session_id"),
Index("ix_flow_instances_tenant_status", "tenant_id", "status"),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
tenant_id: str = Field(..., description="Tenant ID for multi-tenant isolation", index=True)
session_id: str = Field(..., description="Session ID for conversation tracking", index=True)
flow_id: uuid.UUID = Field(
...,
description="Foreign key to script_flows.id",
foreign_key="script_flows.id",
index=True
)
current_step: int = Field(default=1, ge=1, description="Current step number (1-indexed)")
status: str = Field(
default=FlowInstanceStatus.ACTIVE.value,
description="Instance status: active/completed/timeout/cancelled"
)
context: dict[str, Any] | None = Field(
default=None,
sa_column=Column("context", JSON, nullable=True),
description="Flow execution context, stores user inputs"
)
started_at: datetime = Field(default_factory=datetime.utcnow, description="Instance start time")
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
completed_at: datetime | None = Field(default=None, description="Completion time (nullable)")
class FlowStep(SQLModel):
"""[AC-AISVC-71] Schema for a single flow step."""
step_no: int = Field(..., ge=1, description="Step number (1-indexed)")
content: str = Field(..., description="Script content for this step")
wait_input: bool = Field(default=True, description="Whether to wait for user input")
timeout_seconds: int = Field(default=120, ge=1, description="Timeout in seconds")
timeout_action: str = Field(
default=TimeoutAction.REPEAT.value,
description="Action on timeout: repeat/skip/transfer"
)
next_conditions: list[dict[str, Any]] | None = Field(
default=None,
description="Conditions for next step: [{'keywords': [...], 'goto_step': N}]"
)
default_next: int | None = Field(default=None, description="Default next step if no condition matches")
class ScriptFlowCreate(SQLModel):
"""[AC-AISVC-71] Schema for creating a new script flow."""
name: str
description: str | None = None
steps: list[dict[str, Any]]
is_enabled: bool = True
class ScriptFlowUpdate(SQLModel):
"""[AC-AISVC-73] Schema for updating a script flow."""
name: str | None = None
description: str | None = None
steps: list[dict[str, Any]] | None = None
is_enabled: bool | None = None
class FlowAdvanceResult:
"""
[AC-AISVC-75] Result of flow step advancement.
"""
def __init__(
self,
completed: bool,
message: str | None = None,
current_step: int | None = None,
total_steps: int | None = None,
timeout_action: str | None = None,
):
self.completed = completed
self.message = message
self.current_step = current_step
self.total_steps = total_steps
self.timeout_action = timeout_action
def to_dict(self) -> dict[str, Any]:
result = {
"completed": self.completed,
}
if self.message is not None:
result["message"] = self.message
if self.current_step is not None:
result["current_step"] = self.current_step
if self.total_steps is not None:
result["total_steps"] = self.total_steps
if self.timeout_action is not None:
result["timeout_action"] = self.timeout_action
return result