feat: 添加飞书用户模块和订阅功能支持 - 新增feishu_users模块用于处理飞书用户身份验证和权限管理 - 新增subscriptions模块用于处理订阅相关功能 - 新增personalization模块用于个性化服务 - 在alembic迁移配置中注册新的模型模块 - 在API路由器中添加feishu_users和subscriptions路由 - 实现事件调度服务的改进,包括错误处理和状态更新优化 - 添加飞书命令处理的权限检查机制 - 实现飞书应用票据事件处理 - 改进审计日志记录功能 ```
192 lines
6.6 KiB
Python
192 lines
6.6 KiB
Python
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.constants import ActorValue
|
|
from app.modules.ai_memory.constants import AIMemoryScope
|
|
from app.modules.ai_memory.service import AIMemoryService
|
|
from app.modules.business.models import MarketWatchlist
|
|
from app.modules.personalization.constants import (
|
|
PersonalizationContextKey,
|
|
PreferenceCategory,
|
|
)
|
|
from app.modules.personalization.services.conversations import ConversationService
|
|
from app.modules.personalization.services.preferences import PreferenceService
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PersonalizationContext:
|
|
"""Ordered, provider-neutral context sections for one AI request."""
|
|
|
|
system_constraints: str
|
|
company_rules: list[dict[str, Any]]
|
|
personal_rules: list[dict[str, Any]]
|
|
current_request: str
|
|
preferences: list[dict[str, Any]]
|
|
interests: list[dict[str, Any]]
|
|
personal_memory: list[dict[str, Any]]
|
|
conversation_history: list[dict[str, Any]]
|
|
provider_session_id: str | None = field(default=None)
|
|
|
|
def as_ordered_dict(self) -> dict[str, Any]:
|
|
return {
|
|
PersonalizationContextKey.SYSTEM_CONSTRAINTS: self.system_constraints,
|
|
PersonalizationContextKey.COMPANY_RULES: self.company_rules,
|
|
PersonalizationContextKey.PERSONAL_RULES: self.personal_rules,
|
|
PersonalizationContextKey.CURRENT_REQUEST: self.current_request,
|
|
PersonalizationContextKey.PREFERENCES: self.preferences,
|
|
PersonalizationContextKey.INTERESTS: self.interests,
|
|
PersonalizationContextKey.PERSONAL_MEMORY: self.personal_memory,
|
|
PersonalizationContextKey.CONVERSATION_HISTORY: self.conversation_history,
|
|
}
|
|
|
|
|
|
class PersonalizationContextService:
|
|
"""Load only the explicitly requested company and owner-scoped context layers."""
|
|
|
|
def __init__(self, db: Session):
|
|
self.db = db
|
|
self.memory = AIMemoryService(db)
|
|
self.preferences = PreferenceService(db)
|
|
self.conversations = ConversationService(db)
|
|
|
|
def build(
|
|
self,
|
|
*,
|
|
owner_id: int | None,
|
|
request: str,
|
|
system_constraints: str,
|
|
chat_type: str | None = None,
|
|
chat_key: str | None = None,
|
|
scope: str = AIMemoryScope.GLOBAL,
|
|
subject: str | None = None,
|
|
actor: str = ActorValue.SYSTEM,
|
|
include_company_rules: bool = True,
|
|
include_personal_context: bool = True,
|
|
include_history: bool = True,
|
|
) -> PersonalizationContext:
|
|
company_rules = (
|
|
self.memory.active_rules(
|
|
scope=scope,
|
|
subject=subject,
|
|
owner_id=None,
|
|
)
|
|
if include_company_rules
|
|
else []
|
|
)
|
|
personal_rules: list[dict[str, Any]] = []
|
|
preference_items: list[dict[str, Any]] = []
|
|
interests: list[dict[str, Any]] = []
|
|
personal_memory: list[dict[str, Any]] = []
|
|
history: list[dict[str, Any]] = []
|
|
provider_session_id: str | None = None
|
|
|
|
if owner_id is not None and include_personal_context:
|
|
personal_rules = self.memory.active_rules(
|
|
scope=scope,
|
|
subject=subject,
|
|
owner_id=owner_id,
|
|
)
|
|
all_preferences = self.preferences.list_preferences(owner_id)
|
|
interest_categories = {
|
|
PreferenceCategory.TOPIC,
|
|
PreferenceCategory.INTEREST,
|
|
}
|
|
for preference in all_preferences:
|
|
if preference["category"] in interest_categories:
|
|
interests.append(preference)
|
|
else:
|
|
preference_items.append(preference)
|
|
interests.extend(self._watchlist_interests(owner_id))
|
|
personal_memory = self.memory.recall(
|
|
query=request,
|
|
scope=scope,
|
|
subject=subject,
|
|
actor=actor,
|
|
owner_id=owner_id,
|
|
)
|
|
if include_history and chat_type and chat_key:
|
|
history = self.conversations.history(owner_id, chat_type, chat_key)
|
|
provider_session_id = self.conversations.provider_session_id(
|
|
owner_id,
|
|
chat_type,
|
|
chat_key,
|
|
)
|
|
|
|
return PersonalizationContext(
|
|
system_constraints=system_constraints,
|
|
company_rules=company_rules,
|
|
personal_rules=personal_rules,
|
|
current_request=request,
|
|
preferences=preference_items,
|
|
interests=interests,
|
|
personal_memory=personal_memory,
|
|
conversation_history=history,
|
|
provider_session_id=provider_session_id,
|
|
)
|
|
|
|
def build_private_scheduled(
|
|
self,
|
|
*,
|
|
owner_id: int,
|
|
request: str,
|
|
system_constraints: str,
|
|
scope: str = AIMemoryScope.GLOBAL,
|
|
subject: str | None = None,
|
|
actor: str = ActorValue.SYSTEM,
|
|
) -> PersonalizationContext:
|
|
"""Build private scheduled context without company data or conversation history."""
|
|
|
|
return self.build(
|
|
owner_id=owner_id,
|
|
request=request,
|
|
system_constraints=system_constraints,
|
|
scope=scope,
|
|
subject=subject,
|
|
actor=actor,
|
|
include_company_rules=False,
|
|
include_personal_context=True,
|
|
include_history=False,
|
|
)
|
|
|
|
def build_group_scheduled(
|
|
self,
|
|
*,
|
|
request: str,
|
|
system_constraints: str,
|
|
scope: str = AIMemoryScope.GLOBAL,
|
|
subject: str | None = None,
|
|
) -> PersonalizationContext:
|
|
"""Build group scheduled context without any creator profile."""
|
|
|
|
return self.build(
|
|
owner_id=None,
|
|
request=request,
|
|
system_constraints=system_constraints,
|
|
scope=scope,
|
|
subject=subject,
|
|
include_company_rules=True,
|
|
include_personal_context=False,
|
|
include_history=False,
|
|
)
|
|
|
|
def _watchlist_interests(self, owner_id: int) -> list[dict[str, Any]]:
|
|
symbols = self.db.execute(
|
|
select(MarketWatchlist.symbol)
|
|
.where(
|
|
MarketWatchlist.owner_id == owner_id,
|
|
MarketWatchlist.enabled.is_(True),
|
|
)
|
|
.order_by(MarketWatchlist.symbol.asc())
|
|
).scalars()
|
|
return [
|
|
{
|
|
"category": "watchlist",
|
|
"value": symbol,
|
|
"source": "market_watchlist",
|
|
}
|
|
for symbol in symbols
|
|
]
|