memorisdk 1.0.2__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of memorisdk might be problematic. Click here for more details.

Files changed (46) hide show
  1. memori/__init__.py +24 -8
  2. memori/agents/conscious_agent.py +252 -414
  3. memori/agents/memory_agent.py +487 -224
  4. memori/agents/retrieval_agent.py +416 -60
  5. memori/config/memory_manager.py +323 -0
  6. memori/core/conversation.py +393 -0
  7. memori/core/database.py +386 -371
  8. memori/core/memory.py +1638 -531
  9. memori/core/providers.py +217 -0
  10. memori/database/adapters/__init__.py +10 -0
  11. memori/database/adapters/mysql_adapter.py +331 -0
  12. memori/database/adapters/postgresql_adapter.py +291 -0
  13. memori/database/adapters/sqlite_adapter.py +229 -0
  14. memori/database/auto_creator.py +320 -0
  15. memori/database/connection_utils.py +207 -0
  16. memori/database/connectors/base_connector.py +283 -0
  17. memori/database/connectors/mysql_connector.py +240 -18
  18. memori/database/connectors/postgres_connector.py +277 -4
  19. memori/database/connectors/sqlite_connector.py +178 -3
  20. memori/database/models.py +400 -0
  21. memori/database/queries/base_queries.py +1 -1
  22. memori/database/queries/memory_queries.py +91 -2
  23. memori/database/query_translator.py +222 -0
  24. memori/database/schema_generators/__init__.py +7 -0
  25. memori/database/schema_generators/mysql_schema_generator.py +215 -0
  26. memori/database/search/__init__.py +8 -0
  27. memori/database/search/mysql_search_adapter.py +255 -0
  28. memori/database/search/sqlite_search_adapter.py +180 -0
  29. memori/database/search_service.py +548 -0
  30. memori/database/sqlalchemy_manager.py +839 -0
  31. memori/integrations/__init__.py +36 -11
  32. memori/integrations/litellm_integration.py +340 -6
  33. memori/integrations/openai_integration.py +506 -240
  34. memori/utils/input_validator.py +395 -0
  35. memori/utils/pydantic_models.py +138 -36
  36. memori/utils/query_builder.py +530 -0
  37. memori/utils/security_audit.py +594 -0
  38. memori/utils/security_integration.py +339 -0
  39. memori/utils/transaction_manager.py +547 -0
  40. {memorisdk-1.0.2.dist-info → memorisdk-2.0.0.dist-info}/METADATA +44 -17
  41. memorisdk-2.0.0.dist-info/RECORD +67 -0
  42. memorisdk-1.0.2.dist-info/RECORD +0 -44
  43. memorisdk-1.0.2.dist-info/entry_points.txt +0 -2
  44. {memorisdk-1.0.2.dist-info → memorisdk-2.0.0.dist-info}/WHEEL +0 -0
  45. {memorisdk-1.0.2.dist-info → memorisdk-2.0.0.dist-info}/licenses/LICENSE +0 -0
  46. {memorisdk-1.0.2.dist-info → memorisdk-2.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,395 @@
1
+ """
2
+ Input validation and sanitization utilities for Memori
3
+ Provides security-focused validation for all database inputs
4
+ """
5
+
6
+ import html
7
+ import json
8
+ import re
9
+ from datetime import datetime
10
+ from typing import Any, Dict, List, Optional, Union
11
+
12
+ from loguru import logger
13
+
14
+ from .exceptions import ValidationError
15
+
16
+
17
+ class InputValidator:
18
+ """Comprehensive input validation and sanitization"""
19
+
20
+ # SQL injection patterns to detect and block
21
+ SQL_INJECTION_PATTERNS = [
22
+ r"(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|UNION)\b)",
23
+ r"(\b(OR|AND)\s+[\w\s]*=[\w\s]*)",
24
+ r"(;|\|\||&&)",
25
+ r"(\-\-|\#|/\*|\*/)",
26
+ r"(\bxp_cmdshell\b|\bsp_executesql\b)",
27
+ r"(\bINTO\s+OUTFILE\b|\bINTO\s+DUMPFILE\b)",
28
+ ]
29
+
30
+ # XSS patterns to detect and sanitize
31
+ XSS_PATTERNS = [
32
+ r"<\s*script[^>]*>.*?</\s*script\s*>",
33
+ r"<\s*iframe[^>]*>.*?</\s*iframe\s*>",
34
+ r"<\s*object[^>]*>.*?</\s*object\s*>",
35
+ r"<\s*embed[^>]*>",
36
+ r"javascript\s*:",
37
+ r"on\w+\s*=",
38
+ ]
39
+
40
+ @classmethod
41
+ def validate_and_sanitize_query(cls, query: str, max_length: int = 10000) -> str:
42
+ """Validate and sanitize search query input"""
43
+ if not isinstance(query, (str, type(None))):
44
+ raise ValidationError("Query must be a string or None")
45
+
46
+ if query is None:
47
+ return ""
48
+
49
+ # Length validation
50
+ if len(query) > max_length:
51
+ raise ValidationError(f"Query too long (max {max_length} characters)")
52
+
53
+ # Check for SQL injection patterns
54
+ query_lower = query.lower()
55
+ for pattern in cls.SQL_INJECTION_PATTERNS:
56
+ if re.search(pattern, query_lower, re.IGNORECASE):
57
+ logger.warning(f"Potential SQL injection attempt blocked: {pattern}")
58
+ raise ValidationError(
59
+ "Invalid query: contains potentially dangerous content"
60
+ )
61
+
62
+ # Check for XSS patterns
63
+ for pattern in cls.XSS_PATTERNS:
64
+ if re.search(pattern, query, re.IGNORECASE):
65
+ logger.warning(f"Potential XSS attempt blocked: {pattern}")
66
+ # Sanitize instead of blocking for XSS
67
+ query = re.sub(pattern, "", query, flags=re.IGNORECASE)
68
+
69
+ # HTML escape for additional safety
70
+ sanitized_query = html.escape(query.strip())
71
+
72
+ return sanitized_query
73
+
74
+ @classmethod
75
+ def validate_namespace(cls, namespace: str) -> str:
76
+ """Validate and sanitize namespace"""
77
+ if not isinstance(namespace, str):
78
+ raise ValidationError("Namespace must be a string")
79
+
80
+ # Namespace validation rules
81
+ sanitized_namespace = namespace.strip()
82
+
83
+ if not sanitized_namespace:
84
+ sanitized_namespace = "default"
85
+
86
+ # Only allow alphanumeric, underscore, hyphen
87
+ if not re.match(r"^[a-zA-Z0-9_\-]+$", sanitized_namespace):
88
+ raise ValidationError(
89
+ "Namespace contains invalid characters (only alphanumeric, underscore, hyphen allowed)"
90
+ )
91
+
92
+ if len(sanitized_namespace) > 100:
93
+ raise ValidationError("Namespace too long (max 100 characters)")
94
+
95
+ return sanitized_namespace
96
+
97
+ @classmethod
98
+ def validate_category_filter(
99
+ cls, category_filter: Optional[List[str]]
100
+ ) -> List[str]:
101
+ """Validate and sanitize category filter list"""
102
+ if category_filter is None:
103
+ return []
104
+
105
+ if not isinstance(category_filter, list):
106
+ raise ValidationError("Category filter must be a list or None")
107
+
108
+ if len(category_filter) > 50: # Reasonable limit
109
+ raise ValidationError("Too many categories in filter (max 50)")
110
+
111
+ sanitized_categories = []
112
+ for category in category_filter:
113
+ if not isinstance(category, str):
114
+ continue # Skip non-string categories
115
+
116
+ sanitized_category = category.strip()
117
+ if not sanitized_category:
118
+ continue # Skip empty categories
119
+
120
+ # Validate category format
121
+ if not re.match(r"^[a-zA-Z0-9_\-\s]+$", sanitized_category):
122
+ logger.warning(f"Invalid category format: {sanitized_category}")
123
+ continue # Skip invalid categories
124
+
125
+ if len(sanitized_category) > 100:
126
+ sanitized_category = sanitized_category[:100] # Truncate if too long
127
+
128
+ sanitized_categories.append(sanitized_category)
129
+
130
+ return sanitized_categories
131
+
132
+ @classmethod
133
+ def validate_limit(cls, limit: Union[int, str]) -> int:
134
+ """Validate and sanitize limit parameter"""
135
+ try:
136
+ int_limit = int(limit)
137
+ except (ValueError, TypeError):
138
+ raise ValidationError("Limit must be a valid integer")
139
+
140
+ # Enforce reasonable bounds
141
+ if int_limit < 1:
142
+ return 1
143
+ elif int_limit > 1000: # Maximum reasonable limit
144
+ return 1000
145
+
146
+ return int_limit
147
+
148
+ @classmethod
149
+ def validate_memory_id(cls, memory_id: str) -> str:
150
+ """Validate memory ID format"""
151
+ if not isinstance(memory_id, str):
152
+ raise ValidationError("Memory ID must be a string")
153
+
154
+ sanitized_id = memory_id.strip()
155
+
156
+ if not sanitized_id:
157
+ raise ValidationError("Memory ID cannot be empty")
158
+
159
+ # UUID-like format validation
160
+ if not re.match(r"^[a-fA-F0-9\-]{36}$", sanitized_id):
161
+ # Also allow shorter alphanumeric IDs for flexibility
162
+ if not re.match(r"^[a-zA-Z0-9_\-]+$", sanitized_id):
163
+ raise ValidationError("Invalid memory ID format")
164
+
165
+ if len(sanitized_id) > 100:
166
+ raise ValidationError("Memory ID too long")
167
+
168
+ return sanitized_id
169
+
170
+ @classmethod
171
+ def validate_json_field(cls, json_data: Any, field_name: str = "data") -> str:
172
+ """Validate and sanitize JSON data"""
173
+ if json_data is None:
174
+ return "{}"
175
+
176
+ try:
177
+ if isinstance(json_data, str):
178
+ # Validate it's proper JSON
179
+ parsed_data = json.loads(json_data)
180
+ # Re-serialize to ensure clean format
181
+ clean_json = json.dumps(
182
+ parsed_data, ensure_ascii=True, separators=(",", ":")
183
+ )
184
+ else:
185
+ # Serialize Python object to JSON
186
+ clean_json = json.dumps(
187
+ json_data, ensure_ascii=True, separators=(",", ":")
188
+ )
189
+
190
+ # Size limit check (1MB for JSON data)
191
+ if len(clean_json) > 1024 * 1024:
192
+ raise ValidationError(f"{field_name} JSON too large (max 1MB)")
193
+
194
+ return clean_json
195
+
196
+ except (json.JSONDecodeError, TypeError) as e:
197
+ raise ValidationError(f"Invalid JSON in {field_name}: {e}")
198
+
199
+ @classmethod
200
+ def validate_text_content(
201
+ cls, content: str, field_name: str = "content", max_length: int = 100000
202
+ ) -> str:
203
+ """Validate and sanitize text content"""
204
+ if not isinstance(content, str):
205
+ raise ValidationError(f"{field_name} must be a string")
206
+
207
+ # Length check
208
+ if len(content) > max_length:
209
+ raise ValidationError(
210
+ f"{field_name} too long (max {max_length} characters)"
211
+ )
212
+
213
+ # XSS sanitization
214
+ sanitized_content = content
215
+ for pattern in cls.XSS_PATTERNS:
216
+ sanitized_content = re.sub(
217
+ pattern, "", sanitized_content, flags=re.IGNORECASE
218
+ )
219
+
220
+ # Basic HTML escaping for storage
221
+ sanitized_content = html.escape(sanitized_content)
222
+
223
+ return sanitized_content.strip()
224
+
225
+ @classmethod
226
+ def validate_timestamp(cls, timestamp: Union[datetime, str, None]) -> datetime:
227
+ """Validate and normalize timestamp"""
228
+ if timestamp is None:
229
+ return datetime.now()
230
+
231
+ if isinstance(timestamp, datetime):
232
+ # Make timezone-naive for SQLite compatibility
233
+ return timestamp.replace(tzinfo=None)
234
+
235
+ if isinstance(timestamp, str):
236
+ try:
237
+ # Try to parse ISO format
238
+ parsed_timestamp = datetime.fromisoformat(
239
+ timestamp.replace("Z", "+00:00")
240
+ )
241
+ return parsed_timestamp.replace(tzinfo=None)
242
+ except ValueError:
243
+ raise ValidationError("Invalid timestamp format (use ISO format)")
244
+
245
+ raise ValidationError("Timestamp must be datetime object, ISO string, or None")
246
+
247
+ @classmethod
248
+ def validate_score(
249
+ cls, score: Union[float, int, str], field_name: str = "score"
250
+ ) -> float:
251
+ """Validate and normalize score values (0.0 to 1.0)"""
252
+ try:
253
+ float_score = float(score)
254
+ except (ValueError, TypeError):
255
+ raise ValidationError(f"{field_name} must be a valid number")
256
+
257
+ # Clamp to valid range
258
+ if float_score < 0.0:
259
+ return 0.0
260
+ elif float_score > 1.0:
261
+ return 1.0
262
+
263
+ return float_score
264
+
265
+ @classmethod
266
+ def validate_boolean_field(cls, value: Any, field_name: str = "field") -> bool:
267
+ """Validate and convert boolean field"""
268
+ if isinstance(value, bool):
269
+ return value
270
+
271
+ if isinstance(value, int):
272
+ return bool(value)
273
+
274
+ if isinstance(value, str):
275
+ return value.lower() in ("true", "1", "yes", "on")
276
+
277
+ return False # Default to False for safety
278
+
279
+ @classmethod
280
+ def sanitize_sql_identifier(cls, identifier: str) -> str:
281
+ """Sanitize SQL identifiers (table names, column names)"""
282
+ if not isinstance(identifier, str):
283
+ raise ValidationError("SQL identifier must be a string")
284
+
285
+ # Remove dangerous characters and validate format
286
+ sanitized = re.sub(r"[^a-zA-Z0-9_]", "", identifier)
287
+
288
+ if not sanitized or not re.match(r"^[a-zA-Z][a-zA-Z0-9_]*$", sanitized):
289
+ raise ValidationError("Invalid SQL identifier format")
290
+
291
+ if len(sanitized) > 64: # SQL standard limit
292
+ raise ValidationError("SQL identifier too long")
293
+
294
+ # Block reserved words (basic list)
295
+ reserved_words = {
296
+ "SELECT",
297
+ "INSERT",
298
+ "UPDATE",
299
+ "DELETE",
300
+ "DROP",
301
+ "CREATE",
302
+ "ALTER",
303
+ "TABLE",
304
+ "DATABASE",
305
+ "INDEX",
306
+ "VIEW",
307
+ "TRIGGER",
308
+ "PROCEDURE",
309
+ "FUNCTION",
310
+ "EXEC",
311
+ "EXECUTE",
312
+ "UNION",
313
+ "WHERE",
314
+ "FROM",
315
+ "JOIN",
316
+ }
317
+
318
+ if sanitized.upper() in reserved_words:
319
+ raise ValidationError(
320
+ f"Cannot use reserved word as identifier: {sanitized}"
321
+ )
322
+
323
+ return sanitized
324
+
325
+
326
+ class DatabaseInputValidator:
327
+ """Database-specific input validation"""
328
+
329
+ @classmethod
330
+ def validate_insert_params(
331
+ cls, table: str, params: Dict[str, Any]
332
+ ) -> Dict[str, Any]:
333
+ """Validate parameters for database insert operations"""
334
+ sanitized_params = {}
335
+
336
+ # Validate table name
337
+ InputValidator.sanitize_sql_identifier(table)
338
+
339
+ for key, value in params.items():
340
+ # Validate column names
341
+ sanitized_key = InputValidator.sanitize_sql_identifier(key)
342
+
343
+ # Type-specific validation
344
+ if key.endswith("_id"):
345
+ if value is not None:
346
+ sanitized_params[sanitized_key] = InputValidator.validate_memory_id(
347
+ str(value)
348
+ )
349
+ else:
350
+ sanitized_params[sanitized_key] = None
351
+ elif key == "namespace":
352
+ sanitized_params[sanitized_key] = InputValidator.validate_namespace(
353
+ str(value)
354
+ )
355
+ elif key.endswith("_score"):
356
+ sanitized_params[sanitized_key] = InputValidator.validate_score(
357
+ value, key
358
+ )
359
+ elif key.endswith("_at") or key == "timestamp":
360
+ sanitized_params[sanitized_key] = InputValidator.validate_timestamp(
361
+ value
362
+ )
363
+ elif key.endswith("_json") or key == "metadata":
364
+ sanitized_params[sanitized_key] = InputValidator.validate_json_field(
365
+ value, key
366
+ )
367
+ elif isinstance(value, bool) or key.startswith("is_"):
368
+ sanitized_params[sanitized_key] = InputValidator.validate_boolean_field(
369
+ value, key
370
+ )
371
+ elif isinstance(value, str):
372
+ sanitized_params[sanitized_key] = InputValidator.validate_text_content(
373
+ value, key, max_length=50000
374
+ )
375
+ else:
376
+ # Pass through numeric and other safe types
377
+ sanitized_params[sanitized_key] = value
378
+
379
+ return sanitized_params
380
+
381
+ @classmethod
382
+ def validate_search_params(
383
+ cls,
384
+ query: str,
385
+ namespace: str,
386
+ category_filter: Optional[List[str]],
387
+ limit: int,
388
+ ) -> Dict[str, Any]:
389
+ """Validate all search parameters together"""
390
+ return {
391
+ "query": InputValidator.validate_and_sanitize_query(query),
392
+ "namespace": InputValidator.validate_namespace(namespace),
393
+ "category_filter": InputValidator.validate_category_filter(category_filter),
394
+ "limit": InputValidator.validate_limit(limit),
395
+ }
@@ -19,6 +19,26 @@ class MemoryCategoryType(str, Enum):
19
19
  rule = "rule"
20
20
 
21
21
 
22
+ class MemoryClassification(str, Enum):
23
+ """Enhanced memory classification for long-term storage"""
24
+
25
+ ESSENTIAL = "essential" # Core facts, preferences, skills
26
+ CONTEXTUAL = "contextual" # Project context, ongoing work
27
+ CONVERSATIONAL = "conversational" # Regular chat, questions, discussions
28
+ REFERENCE = "reference" # Code examples, technical references
29
+ PERSONAL = "personal" # User details, relationships, life events
30
+ CONSCIOUS_INFO = "conscious-info" # Direct promotion to short-term context
31
+
32
+
33
+ class MemoryImportanceLevel(str, Enum):
34
+ """Memory importance levels"""
35
+
36
+ CRITICAL = "critical" # Must never be lost
37
+ HIGH = "high" # Very important for context
38
+ MEDIUM = "medium" # Useful to remember
39
+ LOW = "low" # Nice to have context
40
+
41
+
22
42
  class RetentionType(str, Enum):
23
43
  """Memory retention types"""
24
44
 
@@ -122,42 +142,6 @@ class MemoryImportance(BaseModel):
122
142
  )
123
143
 
124
144
 
125
- class ProcessedMemory(BaseModel):
126
- """Complete processed memory with all extracted information"""
127
-
128
- # Core categorization
129
- category: MemoryCategory
130
-
131
- # Entity extraction
132
- entities: ExtractedEntities
133
-
134
- # Importance and retention
135
- importance: MemoryImportance
136
-
137
- # Content processing
138
- summary: str = Field(description="Concise, searchable summary of the memory")
139
- searchable_content: str = Field(
140
- description="Content optimized for keyword and semantic search"
141
- )
142
- key_insights: List[str] = Field(
143
- default_factory=list, description="Key insights or takeaways"
144
- )
145
-
146
- # Storage decision
147
- should_store: bool = Field(description="Whether this memory should be stored")
148
- storage_reasoning: str = Field(
149
- description="Why this memory should or shouldn't be stored"
150
- )
151
-
152
- # Metadata (optional fields)
153
- timestamp: Optional[datetime] = Field(
154
- default_factory=datetime.now, description="When this memory was processed"
155
- )
156
- processing_metadata: Optional[Dict[str, str]] = Field(
157
- default=None, description="Additional processing metadata"
158
- )
159
-
160
-
161
145
  class MemorySearchQuery(BaseModel):
162
146
  """Structured query for memory search"""
163
147
 
@@ -244,6 +228,124 @@ class ConversationContext(BaseModel):
244
228
  )
245
229
 
246
230
 
231
+ class ProcessedMemory(BaseModel):
232
+ """Legacy processed memory model for backward compatibility"""
233
+
234
+ content: str = Field(description="The actual memory content")
235
+ summary: str = Field(description="Concise summary for search")
236
+ searchable_content: str = Field(description="Optimized content for search")
237
+ should_store: bool = Field(description="Whether this memory should be stored")
238
+ storage_reasoning: str = Field(
239
+ description="Why this memory should or shouldn't be stored"
240
+ )
241
+ timestamp: datetime = Field(default_factory=datetime.now)
242
+ processing_metadata: Optional[Dict[str, str]] = Field(default=None)
243
+
244
+
245
+ class ProcessedLongTermMemory(BaseModel):
246
+ """Enhanced long-term memory with classification and conscious context"""
247
+
248
+ # Core Memory Content
249
+ content: str = Field(description="The actual memory content")
250
+ summary: str = Field(description="Concise summary for search")
251
+ classification: MemoryClassification = Field(description="Type classification")
252
+ importance: MemoryImportanceLevel = Field(description="Importance level")
253
+
254
+ # Context Information
255
+ topic: Optional[str] = Field(default=None, description="Main topic/subject")
256
+ entities: List[str] = Field(
257
+ default_factory=list, description="People, places, technologies mentioned"
258
+ )
259
+ keywords: List[str] = Field(
260
+ default_factory=list, description="Key terms for search"
261
+ )
262
+
263
+ # Conscious Context Flags
264
+ is_user_context: bool = Field(
265
+ default=False, description="Contains user personal info"
266
+ )
267
+ is_preference: bool = Field(default=False, description="User preference/opinion")
268
+ is_skill_knowledge: bool = Field(
269
+ default=False, description="User's abilities/expertise"
270
+ )
271
+ is_current_project: bool = Field(default=False, description="Current work context")
272
+
273
+ # Memory Management
274
+ duplicate_of: Optional[str] = Field(
275
+ default=None, description="Links to original if duplicate"
276
+ )
277
+ supersedes: List[str] = Field(
278
+ default_factory=list, description="Previous memories this replaces"
279
+ )
280
+ related_memories: List[str] = Field(
281
+ default_factory=list, description="Connected memory IDs"
282
+ )
283
+
284
+ # Technical Metadata
285
+ conversation_id: str = Field(description="Source conversation")
286
+ confidence_score: float = Field(
287
+ default=0.8, description="AI confidence in extraction"
288
+ )
289
+ extraction_timestamp: datetime = Field(default_factory=datetime.now)
290
+ last_accessed: Optional[datetime] = Field(default=None)
291
+ access_count: int = Field(default=0)
292
+
293
+ # Classification Reasoning
294
+ classification_reason: str = Field(description="Why this classification was chosen")
295
+ promotion_eligible: bool = Field(
296
+ default=False, description="Should be promoted to short-term"
297
+ )
298
+
299
+ @property
300
+ def importance_score(self) -> float:
301
+ """Convert importance level to numeric score"""
302
+ return {"critical": 0.9, "high": 0.7, "medium": 0.5, "low": 0.3}.get(
303
+ self.importance, 0.5
304
+ )
305
+
306
+
307
+ class UserContextProfile(BaseModel):
308
+ """Permanent user context for conscious ingestion"""
309
+
310
+ # Core Identity
311
+ name: Optional[str] = None
312
+ pronouns: Optional[str] = None
313
+ location: Optional[str] = None
314
+ timezone: Optional[str] = None
315
+
316
+ # Professional Context
317
+ job_title: Optional[str] = None
318
+ company: Optional[str] = None
319
+ industry: Optional[str] = None
320
+ experience_level: Optional[str] = None
321
+ specializations: List[str] = Field(default_factory=list)
322
+
323
+ # Technical Stack
324
+ primary_languages: List[str] = Field(default_factory=list)
325
+ frameworks: List[str] = Field(default_factory=list)
326
+ tools: List[str] = Field(default_factory=list)
327
+ environment: Optional[str] = None
328
+
329
+ # Behavioral Preferences
330
+ communication_style: Optional[str] = None
331
+ technical_depth: Optional[str] = None
332
+ response_preference: Optional[str] = None
333
+
334
+ # Current Context
335
+ active_projects: List[str] = Field(default_factory=list)
336
+ learning_goals: List[str] = Field(default_factory=list)
337
+ domain_expertise: List[str] = Field(default_factory=list)
338
+
339
+ # Values & Constraints
340
+ code_standards: List[str] = Field(default_factory=list)
341
+ time_constraints: Optional[str] = None
342
+ technology_preferences: List[str] = Field(default_factory=list)
343
+
344
+ # Metadata
345
+ last_updated: datetime = Field(default_factory=datetime.now)
346
+ version: int = 1
347
+
348
+
247
349
  class MemoryStats(BaseModel):
248
350
  """Statistics about stored memories"""
249
351