feat(implicit memory): upgrade pydantic v2 compatibility and confidence level handling
- Replace deprecated `.dict()` with `.model_dump(mode='json')` for pydantic v2 compatibility - Convert confidence level from enum-based strings to numerical values (0-100 scale) - Add confidence level mapping in controller (high: 85, medium: 50, low: 20) - Update dimension analyzer to handle both string and numeric confidence inputs - Refactor habit analyzer confidence level validation logic - Remove ConfidenceLevel enum import and replace with integer-based approach - Update memory config validators for numerical confidence level support - Ensure all implicit memory schemas use model_dump for serialization - Improve type consistency across memory analytics modules
This commit is contained in:
@@ -171,7 +171,7 @@ async def get_preference_tags(
|
|||||||
)
|
)
|
||||||
|
|
||||||
api_logger.info(f"Retrieved {len(tags)} preference tags for user: {user_id}")
|
api_logger.info(f"Retrieved {len(tags)} preference tags for user: {user_id}")
|
||||||
return success(data=[tag.dict() for tag in tags], msg="偏好标签获取成功")
|
return success(data=[tag.model_dump(mode='json') for tag in tags], msg="偏好标签获取成功")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return handle_implicit_memory_error(e, "偏好标签获取", user_id)
|
return handle_implicit_memory_error(e, "偏好标签获取", user_id)
|
||||||
@@ -210,7 +210,7 @@ async def get_dimension_portrait(
|
|||||||
)
|
)
|
||||||
|
|
||||||
api_logger.info(f"Dimension portrait retrieved for user: {user_id}")
|
api_logger.info(f"Dimension portrait retrieved for user: {user_id}")
|
||||||
return success(data=portrait.dict(), msg="四维画像获取成功")
|
return success(data=portrait.model_dump(mode='json'), msg="四维画像获取成功")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return handle_implicit_memory_error(e, "四维画像获取", user_id)
|
return handle_implicit_memory_error(e, "四维画像获取", user_id)
|
||||||
@@ -249,7 +249,7 @@ async def get_interest_area_distribution(
|
|||||||
)
|
)
|
||||||
|
|
||||||
api_logger.info(f"Interest area distribution retrieved for user: {user_id}")
|
api_logger.info(f"Interest area distribution retrieved for user: {user_id}")
|
||||||
return success(data=distribution.dict(), msg="兴趣领域分布获取成功")
|
return success(data=distribution.model_dump(mode='json'), msg="兴趣领域分布获取成功")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return handle_implicit_memory_error(e, "兴趣领域分布获取", user_id)
|
return handle_implicit_memory_error(e, "兴趣领域分布获取", user_id)
|
||||||
@@ -283,18 +283,28 @@ async def get_behavior_habits(
|
|||||||
# Validate inputs
|
# Validate inputs
|
||||||
validate_user_id(user_id)
|
validate_user_id(user_id)
|
||||||
|
|
||||||
|
# Convert string confidence level to numerical
|
||||||
|
numerical_confidence = None
|
||||||
|
if confidence_level:
|
||||||
|
confidence_mapping = {
|
||||||
|
"high": 85,
|
||||||
|
"medium": 50,
|
||||||
|
"low": 20
|
||||||
|
}
|
||||||
|
numerical_confidence = confidence_mapping.get(confidence_level.lower())
|
||||||
|
|
||||||
# Create service with user-specific config
|
# Create service with user-specific config
|
||||||
service = ImplicitMemoryService(db=db, end_user_id=user_id)
|
service = ImplicitMemoryService(db=db, end_user_id=user_id)
|
||||||
|
|
||||||
habits = await service.get_behavior_habits(
|
habits = await service.get_behavior_habits(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
confidence_level=confidence_level,
|
confidence_level=numerical_confidence,
|
||||||
frequency_pattern=frequency_pattern,
|
frequency_pattern=frequency_pattern,
|
||||||
time_period=time_period
|
time_period=time_period
|
||||||
)
|
)
|
||||||
|
|
||||||
api_logger.info(f"Retrieved {len(habits)} behavior habits for user: {user_id}")
|
api_logger.info(f"Retrieved {len(habits)} behavior habits for user: {user_id}")
|
||||||
return success(data=[habit.dict() for habit in habits], msg="行为习惯获取成功")
|
return success(data=[habit.model_dump(mode='json') for habit in habits], msg="行为习惯获取成功")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return handle_implicit_memory_error(e, "行为习惯获取", user_id)
|
return handle_implicit_memory_error(e, "行为习惯获取", user_id)
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from typing import Any, Dict, List, Optional
|
|||||||
from app.core.memory.analytics.implicit_memory.llm_client import ImplicitMemoryLLMClient
|
from app.core.memory.analytics.implicit_memory.llm_client import ImplicitMemoryLLMClient
|
||||||
from app.core.memory.llm_tools.llm_client import LLMClientException
|
from app.core.memory.llm_tools.llm_client import LLMClientException
|
||||||
from app.schemas.implicit_memory_schema import (
|
from app.schemas.implicit_memory_schema import (
|
||||||
ConfidenceLevel,
|
|
||||||
DimensionPortrait,
|
DimensionPortrait,
|
||||||
DimensionScore,
|
DimensionScore,
|
||||||
UserMemorySummary,
|
UserMemorySummary,
|
||||||
@@ -28,7 +27,7 @@ class DimensionData(BaseModel):
|
|||||||
percentage: float = Field(ge=0.0, le=100.0)
|
percentage: float = Field(ge=0.0, le=100.0)
|
||||||
evidence: List[str] = Field(default_factory=list)
|
evidence: List[str] = Field(default_factory=list)
|
||||||
reasoning: str = ""
|
reasoning: str = ""
|
||||||
confidence_level: str = "medium"
|
confidence_level: int = 50 # Default to medium confidence
|
||||||
|
|
||||||
|
|
||||||
class DimensionAnalysisResponse(BaseModel):
|
class DimensionAnalysisResponse(BaseModel):
|
||||||
@@ -147,8 +146,7 @@ class DimensionAnalyzer:
|
|||||||
percentage = max(0.0, min(100.0, float(percentage)))
|
percentage = max(0.0, min(100.0, float(percentage)))
|
||||||
|
|
||||||
# Validate confidence level
|
# Validate confidence level
|
||||||
confidence_level_str = dimension_data.get("confidence_level", "low")
|
confidence_level = self._validate_confidence_level(dimension_data.get("confidence_level", 50))
|
||||||
confidence_level = self._validate_confidence_level(confidence_level_str)
|
|
||||||
|
|
||||||
# Ensure evidence is not empty
|
# Ensure evidence is not empty
|
||||||
evidence = dimension_data.get("evidence", [])
|
evidence = dimension_data.get("evidence", [])
|
||||||
@@ -182,32 +180,41 @@ class DimensionAnalyzer:
|
|||||||
percentage=0.0,
|
percentage=0.0,
|
||||||
evidence=["Insufficient data for analysis"],
|
evidence=["Insufficient data for analysis"],
|
||||||
reasoning=f"No clear evidence found for {dimension_name} dimension",
|
reasoning=f"No clear evidence found for {dimension_name} dimension",
|
||||||
confidence_level=ConfidenceLevel.LOW
|
confidence_level=20 # Low confidence as numerical value
|
||||||
)
|
)
|
||||||
|
|
||||||
def _validate_confidence_level(self, confidence_str: str) -> ConfidenceLevel:
|
def _validate_confidence_level(self, confidence_level) -> int:
|
||||||
"""Validate and convert confidence level string.
|
"""Return confidence level as integer, handling both string and numeric inputs.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
confidence_str: Confidence level as string
|
confidence_level: Confidence level (string or numeric)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ConfidenceLevel enum value
|
Confidence level as integer (0-100)
|
||||||
"""
|
"""
|
||||||
if not confidence_str:
|
# If it's already a number, return it as int
|
||||||
return ConfidenceLevel.MEDIUM
|
if isinstance(confidence_level, (int, float)):
|
||||||
|
return int(confidence_level)
|
||||||
|
|
||||||
confidence_str = str(confidence_str).lower().strip()
|
# If it's a string, convert common values to numbers
|
||||||
|
if isinstance(confidence_level, str):
|
||||||
|
confidence_str = confidence_level.lower().strip()
|
||||||
|
if confidence_str in ["high", "높음"]:
|
||||||
|
return 85
|
||||||
|
elif confidence_str in ["medium", "중간"]:
|
||||||
|
return 50
|
||||||
|
elif confidence_str in ["low", "낮음"]:
|
||||||
|
return 20
|
||||||
|
else:
|
||||||
|
# Try to parse as number
|
||||||
|
try:
|
||||||
|
return int(float(confidence_str))
|
||||||
|
except ValueError:
|
||||||
|
logger.warning(f"Unknown confidence level: {confidence_level}, defaulting to medium")
|
||||||
|
return 50
|
||||||
|
|
||||||
if confidence_str in ["high", "높음"]:
|
# Default fallback
|
||||||
return ConfidenceLevel.HIGH
|
return 50
|
||||||
elif confidence_str in ["medium", "중간"]:
|
|
||||||
return ConfidenceLevel.MEDIUM
|
|
||||||
elif confidence_str in ["low", "낮음"]:
|
|
||||||
return ConfidenceLevel.LOW
|
|
||||||
else:
|
|
||||||
logger.warning(f"Unknown confidence level: {confidence_str}, defaulting to medium")
|
|
||||||
return ConfidenceLevel.MEDIUM
|
|
||||||
|
|
||||||
def _create_empty_portrait(self, user_id: str) -> DimensionPortrait:
|
def _create_empty_portrait(self, user_id: str) -> DimensionPortrait:
|
||||||
"""Create an empty dimension portrait when no data is available.
|
"""Create an empty dimension portrait when no data is available.
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ similar habits with confidence scoring.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from app.core.memory.analytics.implicit_memory.llm_client import ImplicitMemoryLLMClient
|
from app.core.memory.analytics.implicit_memory.llm_client import ImplicitMemoryLLMClient
|
||||||
from app.core.memory.llm_tools.llm_client import LLMClientException
|
from app.core.memory.llm_tools.llm_client import LLMClientException
|
||||||
from app.schemas.implicit_memory_schema import (
|
from app.schemas.implicit_memory_schema import (
|
||||||
BehaviorHabit,
|
BehaviorHabit,
|
||||||
ConfidenceLevel,
|
|
||||||
FrequencyPattern,
|
FrequencyPattern,
|
||||||
UserMemorySummary,
|
UserMemorySummary,
|
||||||
)
|
)
|
||||||
@@ -28,7 +27,7 @@ class HabitData(BaseModel):
|
|||||||
habit_description: str
|
habit_description: str
|
||||||
frequency_pattern: str
|
frequency_pattern: str
|
||||||
time_context: str
|
time_context: str
|
||||||
confidence_level: str
|
confidence_level: int = 50 # Default to medium confidence
|
||||||
supporting_summaries: List[str] = Field(default_factory=list)
|
supporting_summaries: List[str] = Field(default_factory=list)
|
||||||
specific_examples: List[str] = Field(default_factory=list)
|
specific_examples: List[str] = Field(default_factory=list)
|
||||||
is_current: bool = True
|
is_current: bool = True
|
||||||
@@ -88,7 +87,6 @@ class HabitAnalyzer:
|
|||||||
|
|
||||||
# Convert to BehaviorHabit objects
|
# Convert to BehaviorHabit objects
|
||||||
behavior_habits = []
|
behavior_habits = []
|
||||||
current_time = datetime.now()
|
|
||||||
|
|
||||||
for habit_data in response.get("habits", []):
|
for habit_data in response.get("habits", []):
|
||||||
try:
|
try:
|
||||||
@@ -105,8 +103,7 @@ class HabitAnalyzer:
|
|||||||
habit_description=habit_data.get("habit_description", ""),
|
habit_description=habit_data.get("habit_description", ""),
|
||||||
frequency_pattern=self._validate_frequency_pattern(habit_data.get("frequency_pattern", "occasional")),
|
frequency_pattern=self._validate_frequency_pattern(habit_data.get("frequency_pattern", "occasional")),
|
||||||
time_context=habit_data.get("time_context", ""),
|
time_context=habit_data.get("time_context", ""),
|
||||||
confidence_level=self._validate_confidence_level(habit_data.get("confidence_level", "medium")),
|
confidence_level=self._validate_confidence_level(habit_data.get("confidence_level", 50)),
|
||||||
supporting_summaries=supporting_summaries,
|
|
||||||
specific_examples=specific_examples,
|
specific_examples=specific_examples,
|
||||||
first_observed=first_observed,
|
first_observed=first_observed,
|
||||||
last_observed=last_observed,
|
last_observed=last_observed,
|
||||||
@@ -165,26 +162,38 @@ class HabitAnalyzer:
|
|||||||
|
|
||||||
return frequency_mapping.get(frequency_str, FrequencyPattern.OCCASIONAL)
|
return frequency_mapping.get(frequency_str, FrequencyPattern.OCCASIONAL)
|
||||||
|
|
||||||
def _validate_confidence_level(self, confidence_str: str) -> ConfidenceLevel:
|
def _validate_confidence_level(self, confidence_level) -> int:
|
||||||
"""Validate and convert confidence level string.
|
"""Return confidence level as integer, handling both string and numeric inputs.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
confidence_str: Confidence level as string
|
confidence_level: Confidence level (string or numeric)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ConfidenceLevel enum value
|
Confidence level as integer (0-100)
|
||||||
"""
|
"""
|
||||||
confidence_str = confidence_str.lower().strip()
|
# If it's already a number, return it as int
|
||||||
|
if isinstance(confidence_level, (int, float)):
|
||||||
|
return int(confidence_level)
|
||||||
|
|
||||||
if confidence_str in ["high", "높음"]:
|
# If it's a string, convert common values to numbers
|
||||||
return ConfidenceLevel.HIGH
|
if isinstance(confidence_level, str):
|
||||||
elif confidence_str in ["medium", "중간"]:
|
confidence_str = confidence_level.lower().strip()
|
||||||
return ConfidenceLevel.MEDIUM
|
if confidence_str in ["high", "높음"]:
|
||||||
elif confidence_str in ["low", "낮음"]:
|
return 85
|
||||||
return ConfidenceLevel.LOW
|
elif confidence_str in ["medium", "중간"]:
|
||||||
else:
|
return 50
|
||||||
logger.warning(f"Unknown confidence level: {confidence_str}, defaulting to medium")
|
elif confidence_str in ["low", "낮음"]:
|
||||||
return ConfidenceLevel.MEDIUM
|
return 20
|
||||||
|
else:
|
||||||
|
# Try to parse as number
|
||||||
|
try:
|
||||||
|
return int(float(confidence_str))
|
||||||
|
except ValueError:
|
||||||
|
logger.warning(f"Unknown confidence level: {confidence_level}, defaulting to medium")
|
||||||
|
return 50
|
||||||
|
|
||||||
|
# Default fallback
|
||||||
|
return 50
|
||||||
|
|
||||||
def _determine_observation_dates(
|
def _determine_observation_dates(
|
||||||
self,
|
self,
|
||||||
@@ -249,7 +258,7 @@ class HabitAnalyzer:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Check supporting summaries
|
# Check supporting summaries
|
||||||
if not habit.supporting_summaries or len(habit.supporting_summaries) == 0:
|
if not habit.specific_examples or len(habit.specific_examples) == 0:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check specific examples
|
# Check specific examples
|
||||||
@@ -389,9 +398,9 @@ class HabitAnalyzer:
|
|||||||
Returns:
|
Returns:
|
||||||
Merged behavioral habit
|
Merged behavioral habit
|
||||||
"""
|
"""
|
||||||
# Combine supporting summaries
|
# Combine supporting summaries (using specific_examples instead)
|
||||||
combined_summaries = list(set(
|
combined_examples = list(set(
|
||||||
existing_habit.supporting_summaries + new_habit.supporting_summaries
|
existing_habit.specific_examples + new_habit.specific_examples
|
||||||
))
|
))
|
||||||
|
|
||||||
# Combine specific examples
|
# Combine specific examples
|
||||||
@@ -400,8 +409,7 @@ class HabitAnalyzer:
|
|||||||
))
|
))
|
||||||
|
|
||||||
# Update confidence level (take higher confidence)
|
# Update confidence level (take higher confidence)
|
||||||
confidence_levels = [existing_habit.confidence_level, new_habit.confidence_level]
|
new_confidence = max(existing_habit.confidence_level, new_habit.confidence_level)
|
||||||
new_confidence = max(confidence_levels, key=lambda x: ["low", "medium", "high"].index(x.value))
|
|
||||||
|
|
||||||
# Update observation dates
|
# Update observation dates
|
||||||
first_observed = min(existing_habit.first_observed, new_habit.first_observed)
|
first_observed = min(existing_habit.first_observed, new_habit.first_observed)
|
||||||
@@ -420,7 +428,6 @@ class HabitAnalyzer:
|
|||||||
frequency_pattern=existing_habit.frequency_pattern, # Keep original frequency
|
frequency_pattern=existing_habit.frequency_pattern, # Keep original frequency
|
||||||
time_context=combined_time_context,
|
time_context=combined_time_context,
|
||||||
confidence_level=new_confidence,
|
confidence_level=new_confidence,
|
||||||
supporting_summaries=combined_summaries,
|
|
||||||
specific_examples=combined_examples,
|
specific_examples=combined_examples,
|
||||||
first_observed=first_observed,
|
first_observed=first_observed,
|
||||||
last_observed=last_observed,
|
last_observed=last_observed,
|
||||||
@@ -437,8 +444,8 @@ class HabitAnalyzer:
|
|||||||
Sorted list of habits
|
Sorted list of habits
|
||||||
"""
|
"""
|
||||||
def priority_score(habit: BehaviorHabit) -> tuple:
|
def priority_score(habit: BehaviorHabit) -> tuple:
|
||||||
# Confidence level score (high=3, medium=2, low=1)
|
# Confidence level score (0-100 scale)
|
||||||
confidence_score = {"high": 3, "medium": 2, "low": 1}.get(habit.confidence_level.value, 1)
|
confidence_score = habit.confidence_level
|
||||||
|
|
||||||
# Recency score (more recent = higher score)
|
# Recency score (more recent = higher score)
|
||||||
days_since_last = (datetime.now() - habit.last_observed).days
|
days_since_last = (datetime.now() - habit.last_observed).days
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ from app.core.memory.analytics.implicit_memory.analyzers.habit_analyzer import (
|
|||||||
from app.core.memory.llm_tools.llm_client import LLMClientException
|
from app.core.memory.llm_tools.llm_client import LLMClientException
|
||||||
from app.schemas.implicit_memory_schema import (
|
from app.schemas.implicit_memory_schema import (
|
||||||
BehaviorHabit,
|
BehaviorHabit,
|
||||||
ConfidenceLevel,
|
|
||||||
FrequencyPattern,
|
FrequencyPattern,
|
||||||
UserMemorySummary,
|
UserMemorySummary,
|
||||||
)
|
)
|
||||||
@@ -116,13 +115,8 @@ class HabitDetector:
|
|||||||
def calculate_ranking_score(habit: BehaviorHabit) -> float:
|
def calculate_ranking_score(habit: BehaviorHabit) -> float:
|
||||||
"""Calculate combined ranking score for a habit."""
|
"""Calculate combined ranking score for a habit."""
|
||||||
|
|
||||||
# Confidence score (0.0-1.0)
|
# Confidence score (0.0-1.0) - convert from 0-100 scale
|
||||||
confidence_scores = {
|
confidence_score = habit.confidence_level / 100.0
|
||||||
ConfidenceLevel.HIGH: 1.0,
|
|
||||||
ConfidenceLevel.MEDIUM: 0.6,
|
|
||||||
ConfidenceLevel.LOW: 0.3
|
|
||||||
}
|
|
||||||
confidence_score = confidence_scores.get(habit.confidence_level, 0.3)
|
|
||||||
|
|
||||||
# Recency score (0.0-1.0)
|
# Recency score (0.0-1.0)
|
||||||
current_time = datetime.now()
|
current_time = datetime.now()
|
||||||
@@ -152,7 +146,7 @@ class HabitDetector:
|
|||||||
frequency_bonus = frequency_bonuses.get(habit.frequency_pattern, 0.0)
|
frequency_bonus = frequency_bonuses.get(habit.frequency_pattern, 0.0)
|
||||||
|
|
||||||
# Evidence quality bonus
|
# Evidence quality bonus
|
||||||
evidence_bonus = min(len(habit.supporting_summaries) / 10.0, 0.1) # Max 0.1 bonus
|
evidence_bonus = min(len(habit.specific_examples) / 10.0, 0.1) # Max 0.1 bonus
|
||||||
|
|
||||||
# Current habit bonus
|
# Current habit bonus
|
||||||
current_bonus = 0.1 if habit.is_current else 0.0
|
current_bonus = 0.1 if habit.is_current else 0.0
|
||||||
@@ -204,7 +198,6 @@ class HabitDetector:
|
|||||||
frequency_pattern=habit.frequency_pattern,
|
frequency_pattern=habit.frequency_pattern,
|
||||||
time_context=habit.time_context,
|
time_context=habit.time_context,
|
||||||
confidence_level=habit.confidence_level,
|
confidence_level=habit.confidence_level,
|
||||||
supporting_summaries=habit.supporting_summaries,
|
|
||||||
specific_examples=habit.specific_examples,
|
specific_examples=habit.specific_examples,
|
||||||
first_observed=habit.first_observed,
|
first_observed=habit.first_observed,
|
||||||
last_observed=habit.last_observed,
|
last_observed=habit.last_observed,
|
||||||
@@ -218,7 +211,6 @@ class HabitDetector:
|
|||||||
frequency_pattern=habit.frequency_pattern,
|
frequency_pattern=habit.frequency_pattern,
|
||||||
time_context=habit.time_context,
|
time_context=habit.time_context,
|
||||||
confidence_level=habit.confidence_level,
|
confidence_level=habit.confidence_level,
|
||||||
supporting_summaries=habit.supporting_summaries,
|
|
||||||
specific_examples=habit.specific_examples,
|
specific_examples=habit.specific_examples,
|
||||||
first_observed=habit.first_observed,
|
first_observed=habit.first_observed,
|
||||||
last_observed=habit.last_observed,
|
last_observed=habit.last_observed,
|
||||||
|
|||||||
@@ -64,6 +64,11 @@ def validate_model_exists_and_active(
|
|||||||
) -> tuple[str, bool]:
|
) -> tuple[str, bool]:
|
||||||
"""Validate that a model exists and is active.
|
"""Validate that a model exists and is active.
|
||||||
|
|
||||||
|
This function performs tenant-aware model validation with detailed error messages:
|
||||||
|
- If model doesn't exist at all: "Model not found"
|
||||||
|
- If model exists but belongs to different tenant: "Model belongs to different tenant" with details
|
||||||
|
- If model exists and accessible but inactive: "Model is inactive"
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
model_id: Model UUID to validate
|
model_id: Model UUID to validate
|
||||||
model_type: Type of model ("llm", "embedding", "rerank")
|
model_type: Type of model ("llm", "embedding", "rerank")
|
||||||
@@ -76,7 +81,7 @@ def validate_model_exists_and_active(
|
|||||||
Tuple of (model_name, is_active)
|
Tuple of (model_name, is_active)
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ModelNotFoundError: If model does not exist
|
ModelNotFoundError: If model does not exist or belongs to different tenant
|
||||||
ModelInactiveError: If model exists but is inactive
|
ModelInactiveError: If model exists but is inactive
|
||||||
"""
|
"""
|
||||||
from app.repositories.model_repository import ModelConfigRepository
|
from app.repositories.model_repository import ModelConfigRepository
|
||||||
@@ -84,21 +89,48 @@ def validate_model_exists_and_active(
|
|||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# First check if model exists at all (without tenant filtering)
|
||||||
|
model_without_tenant = ModelConfigRepository.get_by_id(db, model_id, tenant_id=None)
|
||||||
|
|
||||||
|
# Then check with tenant filtering
|
||||||
model = ModelConfigRepository.get_by_id(db, model_id, tenant_id)
|
model = ModelConfigRepository.get_by_id(db, model_id, tenant_id)
|
||||||
elapsed_ms = (time.time() - start_time) * 1000
|
elapsed_ms = (time.time() - start_time) * 1000
|
||||||
|
|
||||||
if not model:
|
if not model:
|
||||||
logger.warning(
|
if model_without_tenant:
|
||||||
"Model not found",
|
# Model exists but belongs to different tenant
|
||||||
extra={"model_id": str(model_id), "model_type": model_type, "elapsed_ms": elapsed_ms}
|
logger.warning(
|
||||||
)
|
"Model belongs to different tenant",
|
||||||
raise ModelNotFoundError(
|
extra={
|
||||||
model_id=model_id,
|
"model_id": str(model_id),
|
||||||
model_type=model_type,
|
"model_type": model_type,
|
||||||
config_id=config_id,
|
"model_name": model_without_tenant.name,
|
||||||
workspace_id=workspace_id,
|
"model_tenant_id": str(model_without_tenant.tenant_id),
|
||||||
message=f"{model_type.title()} model {model_id} not found"
|
"requested_tenant_id": str(tenant_id),
|
||||||
)
|
"is_public": model_without_tenant.is_public,
|
||||||
|
"elapsed_ms": elapsed_ms
|
||||||
|
}
|
||||||
|
)
|
||||||
|
raise ModelNotFoundError(
|
||||||
|
model_id=model_id,
|
||||||
|
model_type=model_type,
|
||||||
|
config_id=config_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
message=f"{model_type.title()} model {model_id} ({model_without_tenant.name}) belongs to a different tenant (model tenant: {model_without_tenant.tenant_id}, workspace tenant: {tenant_id}). The model is not public and cannot be accessed from this workspace."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Model doesn't exist at all
|
||||||
|
logger.warning(
|
||||||
|
"Model not found",
|
||||||
|
extra={"model_id": str(model_id), "model_type": model_type, "elapsed_ms": elapsed_ms}
|
||||||
|
)
|
||||||
|
raise ModelNotFoundError(
|
||||||
|
model_id=model_id,
|
||||||
|
model_type=model_type,
|
||||||
|
config_id=config_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
message=f"{model_type.title()} model {model_id} not found"
|
||||||
|
)
|
||||||
|
|
||||||
if not model.is_active:
|
if not model.is_active:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
|
|||||||
@@ -7,14 +7,7 @@ import datetime
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator
|
||||||
|
|
||||||
|
|
||||||
class ConfidenceLevel(str, Enum):
|
|
||||||
"""Confidence levels for analysis results."""
|
|
||||||
HIGH = "high"
|
|
||||||
MEDIUM = "medium"
|
|
||||||
LOW = "low"
|
|
||||||
|
|
||||||
|
|
||||||
class FrequencyPattern(str, Enum):
|
class FrequencyPattern(str, Enum):
|
||||||
@@ -41,6 +34,14 @@ class TimeRange(BaseModel):
|
|||||||
raise ValueError('end_date must be after start_date')
|
raise ValueError('end_date must be after start_date')
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
@field_serializer("start_date", when_used="json")
|
||||||
|
def _serialize_start_date(self, dt: datetime.datetime):
|
||||||
|
return int(dt.timestamp() * 1000) if dt else None
|
||||||
|
|
||||||
|
@field_serializer("end_date", when_used="json")
|
||||||
|
def _serialize_end_date(self, dt: datetime.datetime):
|
||||||
|
return int(dt.timestamp() * 1000) if dt else None
|
||||||
|
|
||||||
|
|
||||||
class DateRange(BaseModel):
|
class DateRange(BaseModel):
|
||||||
"""Date range for filtering."""
|
"""Date range for filtering."""
|
||||||
@@ -54,6 +55,14 @@ class DateRange(BaseModel):
|
|||||||
raise ValueError('end_date must be after start_date')
|
raise ValueError('end_date must be after start_date')
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
@field_serializer("start_date", when_used="json")
|
||||||
|
def _serialize_start_date(self, dt: Optional[datetime.datetime]):
|
||||||
|
return int(dt.timestamp() * 1000) if dt else None
|
||||||
|
|
||||||
|
@field_serializer("end_date", when_used="json")
|
||||||
|
def _serialize_end_date(self, dt: Optional[datetime.datetime]):
|
||||||
|
return int(dt.timestamp() * 1000) if dt else None
|
||||||
|
|
||||||
|
|
||||||
class AnalysisConfig(BaseModel):
|
class AnalysisConfig(BaseModel):
|
||||||
"""Configuration for analysis operations."""
|
"""Configuration for analysis operations."""
|
||||||
@@ -79,6 +88,14 @@ class PreferenceTagResponse(BaseModel):
|
|||||||
conversation_references: List[str]
|
conversation_references: List[str]
|
||||||
category: Optional[str] = None
|
category: Optional[str] = None
|
||||||
|
|
||||||
|
@field_serializer("created_at", when_used="json")
|
||||||
|
def _serialize_created_at(self, dt: datetime.datetime):
|
||||||
|
return int(dt.timestamp() * 1000) if dt else None
|
||||||
|
|
||||||
|
@field_serializer("updated_at", when_used="json")
|
||||||
|
def _serialize_updated_at(self, dt: datetime.datetime):
|
||||||
|
return int(dt.timestamp() * 1000) if dt else None
|
||||||
|
|
||||||
|
|
||||||
class DimensionScoreResponse(BaseModel):
|
class DimensionScoreResponse(BaseModel):
|
||||||
"""Score for a personality dimension."""
|
"""Score for a personality dimension."""
|
||||||
@@ -88,7 +105,7 @@ class DimensionScoreResponse(BaseModel):
|
|||||||
percentage: float = Field(ge=0.0, le=100.0)
|
percentage: float = Field(ge=0.0, le=100.0)
|
||||||
evidence: List[str]
|
evidence: List[str]
|
||||||
reasoning: str
|
reasoning: str
|
||||||
confidence_level: ConfidenceLevel
|
confidence_level: int = Field(ge=0, le=100)
|
||||||
|
|
||||||
|
|
||||||
class DimensionPortraitResponse(BaseModel):
|
class DimensionPortraitResponse(BaseModel):
|
||||||
@@ -104,6 +121,10 @@ class DimensionPortraitResponse(BaseModel):
|
|||||||
total_summaries_analyzed: int
|
total_summaries_analyzed: int
|
||||||
historical_trends: Optional[List[Dict[str, Any]]] = None
|
historical_trends: Optional[List[Dict[str, Any]]] = None
|
||||||
|
|
||||||
|
@field_serializer("analysis_timestamp", when_used="json")
|
||||||
|
def _serialize_analysis_timestamp(self, dt: datetime.datetime):
|
||||||
|
return int(dt.timestamp() * 1000) if dt else None
|
||||||
|
|
||||||
|
|
||||||
class InterestCategoryResponse(BaseModel):
|
class InterestCategoryResponse(BaseModel):
|
||||||
"""Interest category with percentage and evidence."""
|
"""Interest category with percentage and evidence."""
|
||||||
@@ -132,6 +153,10 @@ class InterestAreaDistributionResponse(BaseModel):
|
|||||||
"""Calculate total percentage across all interest areas."""
|
"""Calculate total percentage across all interest areas."""
|
||||||
return self.tech.percentage + self.lifestyle.percentage + self.music.percentage + self.art.percentage
|
return self.tech.percentage + self.lifestyle.percentage + self.music.percentage + self.art.percentage
|
||||||
|
|
||||||
|
@field_serializer("analysis_timestamp", when_used="json")
|
||||||
|
def _serialize_analysis_timestamp(self, dt: datetime.datetime):
|
||||||
|
return int(dt.timestamp() * 1000) if dt else None
|
||||||
|
|
||||||
|
|
||||||
class BehaviorHabitResponse(BaseModel):
|
class BehaviorHabitResponse(BaseModel):
|
||||||
"""A behavioral habit identified from conversations."""
|
"""A behavioral habit identified from conversations."""
|
||||||
@@ -140,13 +165,20 @@ class BehaviorHabitResponse(BaseModel):
|
|||||||
habit_description: str
|
habit_description: str
|
||||||
frequency_pattern: FrequencyPattern
|
frequency_pattern: FrequencyPattern
|
||||||
time_context: str
|
time_context: str
|
||||||
confidence_level: ConfidenceLevel
|
confidence_level: int = Field(ge=0, le=100)
|
||||||
supporting_summaries: List[str]
|
|
||||||
first_observed: datetime.datetime
|
first_observed: datetime.datetime
|
||||||
last_observed: datetime.datetime
|
last_observed: datetime.datetime
|
||||||
is_current: bool = True
|
is_current: bool = True
|
||||||
specific_examples: List[str]
|
specific_examples: List[str]
|
||||||
|
|
||||||
|
@field_serializer("first_observed", when_used="json")
|
||||||
|
def _serialize_first_observed(self, dt: datetime.datetime):
|
||||||
|
return int(dt.timestamp() * 1000) if dt else None
|
||||||
|
|
||||||
|
@field_serializer("last_observed", when_used="json")
|
||||||
|
def _serialize_last_observed(self, dt: datetime.datetime):
|
||||||
|
return int(dt.timestamp() * 1000) if dt else None
|
||||||
|
|
||||||
|
|
||||||
class UserProfileResponse(BaseModel):
|
class UserProfileResponse(BaseModel):
|
||||||
"""Comprehensive user profile."""
|
"""Comprehensive user profile."""
|
||||||
@@ -163,6 +195,14 @@ class UserProfileResponse(BaseModel):
|
|||||||
total_summaries_analyzed: int
|
total_summaries_analyzed: int
|
||||||
analysis_completeness_score: float = Field(ge=0.0, le=1.0)
|
analysis_completeness_score: float = Field(ge=0.0, le=1.0)
|
||||||
|
|
||||||
|
@field_serializer("created_at", when_used="json")
|
||||||
|
def _serialize_created_at(self, dt: datetime.datetime):
|
||||||
|
return int(dt.timestamp() * 1000) if dt else None
|
||||||
|
|
||||||
|
@field_serializer("updated_at", when_used="json")
|
||||||
|
def _serialize_updated_at(self, dt: datetime.datetime):
|
||||||
|
return int(dt.timestamp() * 1000) if dt else None
|
||||||
|
|
||||||
|
|
||||||
# Internal/Business Logic Schemas
|
# Internal/Business Logic Schemas
|
||||||
|
|
||||||
@@ -176,6 +216,10 @@ class MemorySummary(BaseModel):
|
|||||||
participants: List[str]
|
participants: List[str]
|
||||||
summary_type: str
|
summary_type: str
|
||||||
|
|
||||||
|
@field_serializer("timestamp", when_used="json")
|
||||||
|
def _serialize_timestamp(self, dt: datetime.datetime):
|
||||||
|
return int(dt.timestamp() * 1000) if dt else None
|
||||||
|
|
||||||
|
|
||||||
class UserMemorySummary(BaseModel):
|
class UserMemorySummary(BaseModel):
|
||||||
"""Memory summary filtered for specific user content."""
|
"""Memory summary filtered for specific user content."""
|
||||||
@@ -188,6 +232,10 @@ class UserMemorySummary(BaseModel):
|
|||||||
confidence_score: float = Field(ge=0.0, le=1.0)
|
confidence_score: float = Field(ge=0.0, le=1.0)
|
||||||
summary_type: str
|
summary_type: str
|
||||||
|
|
||||||
|
@field_serializer("timestamp", when_used="json")
|
||||||
|
def _serialize_timestamp(self, dt: datetime.datetime):
|
||||||
|
return int(dt.timestamp() * 1000) if dt else None
|
||||||
|
|
||||||
|
|
||||||
class SummaryAnalysisResult(BaseModel):
|
class SummaryAnalysisResult(BaseModel):
|
||||||
"""Result of analyzing memory summaries."""
|
"""Result of analyzing memory summaries."""
|
||||||
@@ -201,6 +249,10 @@ class SummaryAnalysisResult(BaseModel):
|
|||||||
analysis_timestamp: datetime.datetime
|
analysis_timestamp: datetime.datetime
|
||||||
summaries_analyzed: List[str]
|
summaries_analyzed: List[str]
|
||||||
|
|
||||||
|
@field_serializer("analysis_timestamp", when_used="json")
|
||||||
|
def _serialize_analysis_timestamp(self, dt: datetime.datetime):
|
||||||
|
return int(dt.timestamp() * 1000) if dt else None
|
||||||
|
|
||||||
|
|
||||||
# Aliases for backward compatibility with existing code
|
# Aliases for backward compatibility with existing code
|
||||||
PreferenceTag = PreferenceTagResponse
|
PreferenceTag = PreferenceTagResponse
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ from app.core.memory.analytics.implicit_memory.habit_detector import HabitDetect
|
|||||||
from app.repositories.neo4j.neo4j_connector import Neo4jConnector
|
from app.repositories.neo4j.neo4j_connector import Neo4jConnector
|
||||||
from app.schemas.implicit_memory_schema import (
|
from app.schemas.implicit_memory_schema import (
|
||||||
BehaviorHabit,
|
BehaviorHabit,
|
||||||
ConfidenceLevel,
|
|
||||||
DateRange,
|
DateRange,
|
||||||
DimensionPortrait,
|
DimensionPortrait,
|
||||||
FrequencyPattern,
|
FrequencyPattern,
|
||||||
@@ -303,7 +302,7 @@ class ImplicitMemoryService:
|
|||||||
async def get_behavior_habits(
|
async def get_behavior_habits(
|
||||||
self,
|
self,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
confidence_level: Optional[str] = None,
|
confidence_level: Optional[int] = None,
|
||||||
frequency_pattern: Optional[str] = None,
|
frequency_pattern: Optional[str] = None,
|
||||||
time_period: Optional[str] = None
|
time_period: Optional[str] = None
|
||||||
) -> List[BehaviorHabit]:
|
) -> List[BehaviorHabit]:
|
||||||
@@ -311,7 +310,7 @@ class ImplicitMemoryService:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_id: Target user ID
|
user_id: Target user ID
|
||||||
confidence_level: Optional confidence level filter ("high", "medium", "low")
|
confidence_level: Optional confidence level filter (0-100)
|
||||||
frequency_pattern: Optional frequency pattern filter
|
frequency_pattern: Optional frequency pattern filter
|
||||||
time_period: Optional time period filter ("current", "past")
|
time_period: Optional time period filter ("current", "past")
|
||||||
|
|
||||||
@@ -338,13 +337,8 @@ class ImplicitMemoryService:
|
|||||||
filtered_habits = []
|
filtered_habits = []
|
||||||
for habit in behavior_habits:
|
for habit in behavior_habits:
|
||||||
# Filter by confidence level
|
# Filter by confidence level
|
||||||
if confidence_level:
|
if confidence_level is not None:
|
||||||
try:
|
if habit.confidence_level < confidence_level:
|
||||||
target_confidence = ConfidenceLevel(confidence_level.lower())
|
|
||||||
if habit.confidence_level != target_confidence:
|
|
||||||
continue
|
|
||||||
except ValueError:
|
|
||||||
logger.warning(f"Invalid confidence level: {confidence_level}")
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Filter by frequency pattern
|
# Filter by frequency pattern
|
||||||
@@ -367,12 +361,8 @@ class ImplicitMemoryService:
|
|||||||
filtered_habits.append(habit)
|
filtered_habits.append(habit)
|
||||||
|
|
||||||
# Sort by confidence level and recency
|
# Sort by confidence level and recency
|
||||||
confidence_order = {"high": 3, "medium": 2, "low": 1}
|
|
||||||
filtered_habits.sort(
|
filtered_habits.sort(
|
||||||
key=lambda x: (
|
key=lambda x: (x.confidence_level, x.last_observed),
|
||||||
confidence_order.get(x.confidence_level.value, 0),
|
|
||||||
x.last_observed
|
|
||||||
),
|
|
||||||
reverse=True
|
reverse=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user