marm-mcp-server 2.1__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.
config/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # Configuration module package
config/settings.py ADDED
@@ -0,0 +1,79 @@
1
+ """Configuration settings for MARM MCP Server."""
2
+
3
+ # Advanced memory system availability flags
4
+ try:
5
+ from sentence_transformers import SentenceTransformer
6
+ SEMANTIC_SEARCH_AVAILABLE = True
7
+ except ImportError:
8
+ SEMANTIC_SEARCH_AVAILABLE = False
9
+ print("WARNING: Semantic search not available. Install: pip install sentence-transformers")
10
+
11
+ # Automation scheduler availability
12
+ try:
13
+ from apscheduler.schedulers.asyncio import AsyncIOScheduler
14
+ SCHEDULER_AVAILABLE = True
15
+ except ImportError:
16
+ SCHEDULER_AVAILABLE = False
17
+ print("WARNING: Scheduler not available. Install: pip install apscheduler")
18
+
19
+ import os
20
+ from pathlib import Path
21
+
22
+ # Database configuration - Official .marm system directory (CLI standard)
23
+ def get_marm_db_path():
24
+ """Get the official MARM database path, respecting environment variable if set"""
25
+ # Check if MARM_DB_PATH environment variable is set (for Docker)
26
+ env_db_path = os.environ.get('MARM_DB_PATH')
27
+ if env_db_path:
28
+ # Ensure the directory exists
29
+ db_dir = Path(env_db_path).parent
30
+ db_dir.mkdir(parents=True, exist_ok=True)
31
+ return env_db_path
32
+
33
+ # Follow professional CLI standard: ~/.marm/ (like ~/.git, ~/.docker, ~/.claude)
34
+ marm_dir = Path.home() / ".marm"
35
+
36
+ # Create .marm directory if it doesn't exist
37
+ marm_dir.mkdir(exist_ok=True)
38
+
39
+ return str(marm_dir / "marm_memory.db")
40
+
41
+ DEFAULT_DB_PATH = get_marm_db_path()
42
+ MAX_DB_CONNECTIONS = 5
43
+
44
+ # Analytics database path
45
+ def get_analytics_db_path():
46
+ """Get the analytics database path, respecting environment variable if set"""
47
+ # Check if MARM_ANALYTICS_DB_PATH environment variable is set
48
+ env_analytics_db_path = os.environ.get('MARM_ANALYTICS_DB_PATH')
49
+ if env_analytics_db_path:
50
+ # Ensure the directory exists
51
+ analytics_dir = os.path.dirname(env_analytics_db_path)
52
+ if analytics_dir:
53
+ os.makedirs(analytics_dir, exist_ok=True)
54
+ return env_analytics_db_path
55
+
56
+ # For Docker, use /app/data, for local use the current directory or user's home
57
+ if os.path.exists('/app/data'):
58
+ # Docker environment
59
+ return '/app/data/marm_usage_analytics.db'
60
+ else:
61
+ # Local development environment
62
+ return 'marm_usage_analytics.db'
63
+
64
+ ANALYTICS_DB_PATH = get_analytics_db_path()
65
+
66
+ # Semantic search configuration
67
+ DEFAULT_SEMANTIC_MODEL = "all-MiniLM-L6-v2"
68
+
69
+ # Rate limiting configuration (for future Pro version flexibility)
70
+ RATE_LIMIT_ENABLED = True
71
+ RATE_LIMIT_DEFAULT_REQUESTS = 60
72
+ RATE_LIMIT_DEFAULT_WINDOW = 60
73
+ RATE_LIMIT_MEMORY_HEAVY_REQUESTS = 20
74
+ RATE_LIMIT_SEARCH_REQUESTS = 30
75
+
76
+ # Server configuration
77
+ SERVER_HOST = "0.0.0.0"
78
+ SERVER_PORT = int(os.environ.get('SERVER_PORT', 8001))
79
+ SERVER_VERSION = "2.1.0"
core/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # Core module package
core/events.py ADDED
@@ -0,0 +1,87 @@
1
+ """Event-driven automation system for MARM MCP Server."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import Dict, List, Callable, Any
6
+
7
+ class MARMEvents:
8
+ """Built-in automation system with full error isolation"""
9
+
10
+ def __init__(self):
11
+ self.listeners: Dict[str, List[Callable]] = {}
12
+ self.failed_callbacks: Dict[str, int] = {} # Track failed callback counts
13
+ self.logger = logging.getLogger(__name__)
14
+
15
+ def on(self, event_type: str, callback):
16
+ """Register event listener"""
17
+ if event_type not in self.listeners:
18
+ self.listeners[event_type] = []
19
+ self.listeners[event_type].append(callback)
20
+
21
+ async def emit(self, event_type: str, data: dict):
22
+ """Trigger automatic actions with full error isolation"""
23
+ if event_type not in self.listeners:
24
+ return
25
+
26
+ callbacks = self.listeners[event_type].copy() # Snapshot to prevent modification issues
27
+ successful_callbacks = 0
28
+ failed_callbacks = 0
29
+
30
+ # Execute each callback in complete isolation
31
+ for i, callback in enumerate(callbacks):
32
+ callback_id = f"{event_type}_{id(callback)}"
33
+
34
+ try:
35
+ # Run callback with timeout protection
36
+ await asyncio.wait_for(callback(data), timeout=30.0)
37
+ successful_callbacks += 1
38
+
39
+ # Reset failure count on success
40
+ if callback_id in self.failed_callbacks:
41
+ del self.failed_callbacks[callback_id]
42
+
43
+ except asyncio.TimeoutError:
44
+ failed_callbacks += 1
45
+ self._log_callback_error(callback_id, "Callback timed out after 30 seconds", event_type)
46
+
47
+ except Exception as e:
48
+ failed_callbacks += 1
49
+ self._log_callback_error(callback_id, str(e), event_type)
50
+
51
+ # Log event completion summary
52
+ if failed_callbacks > 0:
53
+ self.logger.warning(
54
+ f"Event '{event_type}' completed: {successful_callbacks} succeeded, {failed_callbacks} failed"
55
+ )
56
+ else:
57
+ self.logger.debug(f"Event '{event_type}' completed successfully: {successful_callbacks} callbacks")
58
+
59
+ def _log_callback_error(self, callback_id: str, error_msg: str, event_type: str):
60
+ """Log callback errors with failure tracking"""
61
+ # Track failure count
62
+ self.failed_callbacks[callback_id] = self.failed_callbacks.get(callback_id, 0) + 1
63
+ failure_count = self.failed_callbacks[callback_id]
64
+
65
+ # Log with increasing severity based on failure count
66
+ if failure_count == 1:
67
+ self.logger.warning(f"Event callback failed for '{event_type}': {error_msg}")
68
+ elif failure_count <= 5:
69
+ self.logger.error(f"Event callback failed {failure_count} times for '{event_type}': {error_msg}")
70
+ else:
71
+ self.logger.critical(f"Event callback consistently failing ({failure_count} times) for '{event_type}': {error_msg}")
72
+ # Could add auto-disable logic here if needed
73
+
74
+ def get_health_status(self) -> Dict[str, Any]:
75
+ """Get event system health status for monitoring"""
76
+ total_listeners = sum(len(callbacks) for callbacks in self.listeners.values())
77
+ failed_callback_count = len(self.failed_callbacks)
78
+
79
+ return {
80
+ "total_event_types": len(self.listeners),
81
+ "total_listeners": total_listeners,
82
+ "failed_callbacks": failed_callback_count,
83
+ "health_status": "healthy" if failed_callback_count == 0 else "degraded"
84
+ }
85
+
86
+ # Global events system
87
+ events = MARMEvents()
core/memory.py ADDED
@@ -0,0 +1,407 @@
1
+ """Advanced memory system with semantic search and MARM protocol support."""
2
+
3
+ import json
4
+ import sqlite3
5
+ import threading
6
+ import uuid
7
+ import queue
8
+ from datetime import datetime, timezone
9
+ from typing import List, Dict, Optional
10
+ import numpy as np
11
+ import html
12
+ import re
13
+
14
+ # Import configuration
15
+ from config.settings import (
16
+ SEMANTIC_SEARCH_AVAILABLE,
17
+ DEFAULT_DB_PATH,
18
+ MAX_DB_CONNECTIONS,
19
+ DEFAULT_SEMANTIC_MODEL
20
+ )
21
+
22
+ # Try to import sentence transformer if available
23
+ if SEMANTIC_SEARCH_AVAILABLE:
24
+ try:
25
+ from sentence_transformers import SentenceTransformer
26
+ except ImportError:
27
+ SEMANTIC_SEARCH_AVAILABLE = False
28
+
29
+
30
+ class SQLiteConnectionPool:
31
+ """Simple SQLite connection pool for better performance under load"""
32
+
33
+ def __init__(self, db_path: str, max_connections: int = 5):
34
+ self.db_path = db_path
35
+ self.max_connections = max_connections
36
+ self.pool = queue.Queue(maxsize=max_connections)
37
+ self.created_connections = 0
38
+ self.lock = threading.Lock()
39
+
40
+ # Pre-create initial connections
41
+ self._create_initial_connections()
42
+
43
+ def _create_initial_connections(self):
44
+ """Create initial pool of connections"""
45
+ for _ in range(2): # Start with 2 connections
46
+ self._create_connection()
47
+
48
+ def _create_connection(self):
49
+ """Create a new SQLite connection with optimal settings"""
50
+ conn = sqlite3.connect(
51
+ self.db_path,
52
+ check_same_thread=False,
53
+ timeout=20.0, # 20 second timeout
54
+ isolation_level=None # autocommit mode
55
+ )
56
+ # Optimize SQLite settings for concurrent access
57
+ conn.execute('PRAGMA journal_mode=WAL') # Write-Ahead Logging
58
+ conn.execute('PRAGMA synchronous=NORMAL') # Balanced performance/safety
59
+ conn.execute('PRAGMA cache_size=10000') # Larger cache
60
+ conn.execute('PRAGMA temp_store=MEMORY') # In-memory temp tables
61
+
62
+ self.pool.put(conn)
63
+ self.created_connections += 1
64
+
65
+ def get_connection(self):
66
+ """Get a connection from the pool"""
67
+ try:
68
+ # Try to get existing connection
69
+ return self.pool.get(block=False)
70
+ except queue.Empty:
71
+ # Create new connection if under limit
72
+ with self.lock:
73
+ if self.created_connections < self.max_connections:
74
+ self._create_connection()
75
+ return self.pool.get(block=False)
76
+
77
+ # Wait for available connection
78
+ return self.pool.get(block=True, timeout=10)
79
+
80
+ def return_connection(self, conn):
81
+ """Return connection to pool"""
82
+ try:
83
+ self.pool.put(conn, block=False)
84
+ except queue.Full:
85
+ # Pool is full, close the connection
86
+ conn.close()
87
+
88
+ def close_all(self):
89
+ """Close all connections in the pool"""
90
+ while not self.pool.empty():
91
+ try:
92
+ conn = self.pool.get(block=False)
93
+ conn.close()
94
+ except queue.Empty:
95
+ break
96
+
97
+
98
+ def sanitize_content(content: str) -> str:
99
+ """Sanitize content to prevent XSS attacks while preserving readability"""
100
+ if not content:
101
+ return content
102
+
103
+ # Remove or neutralize common XSS patterns first (before HTML escaping)
104
+ sanitized = content
105
+
106
+ # Remove script tags entirely
107
+ sanitized = re.sub(r'<script[^>]*>.*?</script>', '', sanitized, flags=re.IGNORECASE | re.DOTALL)
108
+
109
+ # Remove javascript: protocols
110
+ sanitized = re.sub(r'javascript:', 'blocked-javascript:', sanitized, flags=re.IGNORECASE)
111
+
112
+ # Remove on* event handlers (onclick, onload, etc.)
113
+ sanitized = re.sub(r'\son\w+\s*=\s*["\'][^"\']*["\']', '', sanitized, flags=re.IGNORECASE)
114
+
115
+ # Finally, HTML escape any remaining dangerous characters
116
+ sanitized = html.escape(sanitized)
117
+
118
+ return sanitized
119
+
120
+ class MARMMemory:
121
+ """Advanced memory system with semantic search and MARM protocol support"""
122
+
123
+ def __init__(self, db_path: str = DEFAULT_DB_PATH):
124
+ self.db_path = db_path
125
+ self.db_lock = threading.Lock()
126
+
127
+ # Initialize connection pool with configurable settings
128
+ self.connection_pool = SQLiteConnectionPool(db_path, max_connections=MAX_DB_CONNECTIONS)
129
+
130
+ # Lazy loading for semantic search model
131
+ self.encoder = None
132
+ self._encoder_loading = False
133
+ self._encoder_failed = False
134
+
135
+ self.init_database()
136
+
137
+ # Active sessions and notebook state
138
+ self.active_sessions = {}
139
+ self.active_notebook_entries = []
140
+
141
+ def get_connection(self):
142
+ """Context manager for getting database connections from pool"""
143
+ class ConnectionContext:
144
+ def __init__(self, pool):
145
+ self.pool = pool
146
+ self.conn = None
147
+
148
+ def __enter__(self):
149
+ self.conn = self.pool.get_connection()
150
+ return self.conn
151
+
152
+ def __exit__(self, exc_type, exc_val, exc_tb):
153
+ if self.conn:
154
+ if exc_type is None:
155
+ # Successful transaction
156
+ self.conn.commit()
157
+ else:
158
+ # Error occurred, rollback
159
+ self.conn.rollback()
160
+ self.pool.return_connection(self.conn)
161
+
162
+ return ConnectionContext(self.connection_pool)
163
+
164
+ def init_database(self):
165
+ """Initialize SQLite database with all MARM tables"""
166
+ with sqlite3.connect(self.db_path) as conn:
167
+ # Main memories table
168
+ conn.execute('''
169
+ CREATE TABLE IF NOT EXISTS memories (
170
+ id TEXT PRIMARY KEY,
171
+ session_name TEXT NOT NULL,
172
+ content TEXT NOT NULL,
173
+ embedding BLOB,
174
+ timestamp TEXT NOT NULL,
175
+ context_type TEXT DEFAULT 'general',
176
+ metadata TEXT DEFAULT '{}',
177
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
178
+ )
179
+ ''')
180
+
181
+ # Sessions table
182
+ conn.execute('''
183
+ CREATE TABLE IF NOT EXISTS sessions (
184
+ session_name TEXT PRIMARY KEY,
185
+ marm_active BOOLEAN DEFAULT FALSE,
186
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
187
+ last_accessed TEXT DEFAULT CURRENT_TIMESTAMP,
188
+ metadata TEXT DEFAULT '{}'
189
+ )
190
+ ''')
191
+
192
+ # Log entries table (MARM protocol specific)
193
+ conn.execute('''
194
+ CREATE TABLE IF NOT EXISTS log_entries (
195
+ id TEXT PRIMARY KEY,
196
+ session_name TEXT NOT NULL,
197
+ entry_date TEXT NOT NULL,
198
+ topic TEXT NOT NULL,
199
+ summary TEXT NOT NULL,
200
+ full_entry TEXT NOT NULL,
201
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
202
+ )
203
+ ''')
204
+
205
+ # Notebook entries table
206
+ conn.execute('''
207
+ CREATE TABLE IF NOT EXISTS notebook_entries (
208
+ name TEXT PRIMARY KEY,
209
+ data TEXT NOT NULL,
210
+ embedding BLOB,
211
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
212
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
213
+ )
214
+ ''')
215
+
216
+ # User settings table
217
+ conn.execute('''
218
+ CREATE TABLE IF NOT EXISTS user_settings (
219
+ key TEXT PRIMARY KEY,
220
+ value TEXT NOT NULL,
221
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
222
+ )
223
+ ''')
224
+
225
+ conn.commit()
226
+
227
+ def _load_encoder_lazily(self) -> bool:
228
+ """Lazy load the semantic search model only when needed"""
229
+ if self.encoder is not None or self._encoder_failed:
230
+ return self.encoder is not None
231
+
232
+ if self._encoder_loading:
233
+ return False
234
+
235
+ if not SEMANTIC_SEARCH_AVAILABLE:
236
+ self._encoder_failed = True
237
+ return False
238
+
239
+ try:
240
+ self._encoder_loading = True
241
+ print(f"🔄 Loading semantic search model ({DEFAULT_SEMANTIC_MODEL}) for memory system...")
242
+
243
+ from sentence_transformers import SentenceTransformer
244
+ self.encoder = SentenceTransformer(DEFAULT_SEMANTIC_MODEL)
245
+
246
+ print("✅ Semantic search model loaded successfully")
247
+ return True
248
+
249
+ except Exception as e:
250
+ print(f"⚠️ Failed to load semantic search model: {e}")
251
+ print("🔄 Falling back to text-based search")
252
+ self._encoder_failed = True
253
+ return False
254
+ finally:
255
+ self._encoder_loading = False
256
+
257
+ async def auto_classify_content(self, content: str) -> str:
258
+ """Auto-classify content type based on keywords"""
259
+ content_lower = content.lower()
260
+
261
+ if any(word in content_lower for word in ['function', 'class', 'code', 'bug', 'debug', 'error', 'fix', 'implement']):
262
+ return 'code'
263
+ elif any(word in content_lower for word in ['project', 'milestone', 'deadline', 'goal', 'sprint', 'task']):
264
+ return 'project'
265
+ elif any(word in content_lower for word in ['character', 'story', 'plot', 'chapter', 'write', 'book']):
266
+ return 'book'
267
+ else:
268
+ return 'general'
269
+
270
+ async def store_memory(self, content: str, session: str, context_type: str = "general", metadata: Dict = None) -> str:
271
+ """Store content with vector embedding for semantic search"""
272
+ # Sanitize content to prevent XSS attacks
273
+ sanitized_content = sanitize_content(content)
274
+
275
+ if context_type == "general":
276
+ context_type = await self.auto_classify_content(sanitized_content)
277
+
278
+ memory_id = str(uuid.uuid4())
279
+ timestamp = datetime.now(timezone.utc).isoformat()
280
+ metadata = metadata or {}
281
+
282
+ # Generate embedding for semantic search (lazy load encoder if needed)
283
+ embedding_bytes = None
284
+ if sanitized_content.strip() and self._load_encoder_lazily():
285
+ try:
286
+ embedding = self.encoder.encode(sanitized_content)
287
+ embedding_bytes = embedding.tobytes()
288
+ except Exception as e:
289
+ print(f"Failed to generate embedding: {e}")
290
+
291
+ with self.get_connection() as conn:
292
+ # Store sanitized memory
293
+ conn.execute('''
294
+ INSERT INTO memories (id, session_name, content, embedding, timestamp, context_type, metadata)
295
+ VALUES (?, ?, ?, ?, ?, ?, ?)
296
+ ''', (memory_id, session, sanitized_content, embedding_bytes, timestamp, context_type, json.dumps(metadata)))
297
+
298
+ # Update session access time
299
+ conn.execute('''
300
+ INSERT OR REPLACE INTO sessions (session_name, last_accessed)
301
+ VALUES (?, ?)
302
+ ''', (session, timestamp))
303
+
304
+ # Note: events system will be handled by the main application
305
+ # We don't import it here to avoid circular dependencies
306
+
307
+ return memory_id
308
+
309
+ async def recall_similar(self, query: str, session: str = None, limit: int = 5) -> List[Dict]:
310
+ """Find semantically similar memories"""
311
+ if not self._load_encoder_lazily():
312
+ return await self.recall_text_search(query, session, limit)
313
+
314
+ try:
315
+ query_embedding = self.encoder.encode(query)
316
+
317
+ with self.get_connection() as conn:
318
+ # If session is None, search all sessions
319
+ if session is None:
320
+ cursor = conn.execute('''
321
+ SELECT id, session_name, content, embedding, timestamp, context_type, metadata
322
+ FROM memories
323
+ WHERE embedding IS NOT NULL
324
+ ORDER BY timestamp DESC
325
+ LIMIT 1000
326
+ ''')
327
+ else:
328
+ cursor = conn.execute('''
329
+ SELECT id, session_name, content, embedding, timestamp, context_type, metadata
330
+ FROM memories
331
+ WHERE embedding IS NOT NULL
332
+ AND session_name = ?
333
+ ORDER BY timestamp DESC
334
+ LIMIT 1000
335
+ ''', (session,))
336
+
337
+ memories = cursor.fetchall()
338
+ similarities = []
339
+
340
+ for memory in memories:
341
+ try:
342
+ memory_embedding = np.frombuffer(memory[3], dtype=np.float32)
343
+ similarity = np.dot(query_embedding, memory_embedding) / (
344
+ np.linalg.norm(query_embedding) * np.linalg.norm(memory_embedding)
345
+ )
346
+ similarities.append((memory, similarity))
347
+ except Exception:
348
+ continue
349
+
350
+ similarities.sort(key=lambda x: x[1], reverse=True)
351
+
352
+ results = []
353
+ for memory, similarity in similarities[:limit]:
354
+ results.append({
355
+ "id": memory[0],
356
+ "session_name": memory[1],
357
+ "content": memory[2],
358
+ "timestamp": memory[4],
359
+ "context_type": memory[5],
360
+ "metadata": json.loads(memory[6]) if memory[6] else {},
361
+ "similarity": float(similarity)
362
+ })
363
+
364
+ return results
365
+
366
+ except Exception as e:
367
+ print(f"Semantic search failed: {e}")
368
+ return await self.recall_text_search(query, session, limit)
369
+
370
+ async def recall_text_search(self, query: str, session: str = None, limit: int = 5) -> List[Dict]:
371
+ """Fallback text-based search"""
372
+ with self.get_connection() as conn:
373
+ # If session is None, search all sessions
374
+ if session is None:
375
+ cursor = conn.execute('''
376
+ SELECT id, session_name, content, timestamp, context_type, metadata
377
+ FROM memories
378
+ WHERE content LIKE ?
379
+ ORDER BY timestamp DESC
380
+ LIMIT ?
381
+ ''', (f"%{query}%", limit))
382
+ else:
383
+ cursor = conn.execute('''
384
+ SELECT id, session_name, content, timestamp, context_type, metadata
385
+ FROM memories
386
+ WHERE content LIKE ?
387
+ AND session_name = ?
388
+ ORDER BY timestamp DESC
389
+ LIMIT ?
390
+ ''', (f"%{query}%", session, limit))
391
+
392
+ results = []
393
+ for row in cursor.fetchall():
394
+ results.append({
395
+ "id": row[0],
396
+ "session_name": row[1],
397
+ "content": row[2],
398
+ "timestamp": row[3],
399
+ "context_type": row[4],
400
+ "metadata": json.loads(row[5]) if row[5] else {},
401
+ "similarity": 0.8 # Default similarity for text matches
402
+ })
403
+
404
+ return results
405
+
406
+ # Global memory instance
407
+ memory = MARMMemory()
core/models.py ADDED
@@ -0,0 +1,39 @@
1
+ """Pydantic models for MARM MCP Server endpoints."""
2
+
3
+ from pydantic import BaseModel, Field
4
+ from typing import Optional
5
+
6
+
7
+ class SessionRequest(BaseModel):
8
+ session_name: str = Field(..., description="Name of the session")
9
+
10
+
11
+ class LogEntryRequest(BaseModel):
12
+ entry: str = Field(..., description="Log entry in format: YYYY-MM-DD-topic-summary")
13
+ session_name: str = Field(default="main", description="Session name")
14
+
15
+
16
+ class NotebookAddRequest(BaseModel):
17
+ name: str = Field(..., description="Name of the notebook entry")
18
+ data: str = Field(..., description="Content of the notebook entry")
19
+
20
+
21
+ class NotebookUseRequest(BaseModel):
22
+ names: str = Field(..., description="Comma-separated list of notebook entry names")
23
+
24
+
25
+ class ContextBridgeRequest(BaseModel):
26
+ new_topic: str = Field(..., description="New topic for context bridging")
27
+ session_name: str = Field(default="main", description="Session name")
28
+
29
+
30
+ class SmartRecallRequest(BaseModel):
31
+ query: str = Field(..., description="Query to search for in memory")
32
+ session_name: str = Field(default="main", description="Session to search in")
33
+ limit: int = Field(default=5, description="Maximum number of results")
34
+ search_all: bool = Field(default=False, description="Search across all sessions if True")
35
+
36
+
37
+ class ContextualLogRequest(BaseModel):
38
+ content: str = Field(..., description="Content to log with auto-classification")
39
+ session_name: str = Field(default="main", description="Session to log to")