cortex-llm 1.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.
Files changed (48) hide show
  1. cortex/__init__.py +73 -0
  2. cortex/__main__.py +83 -0
  3. cortex/config.py +329 -0
  4. cortex/conversation_manager.py +468 -0
  5. cortex/fine_tuning/__init__.py +8 -0
  6. cortex/fine_tuning/dataset.py +332 -0
  7. cortex/fine_tuning/mlx_lora_trainer.py +502 -0
  8. cortex/fine_tuning/trainer.py +957 -0
  9. cortex/fine_tuning/wizard.py +707 -0
  10. cortex/gpu_validator.py +467 -0
  11. cortex/inference_engine.py +727 -0
  12. cortex/metal/__init__.py +275 -0
  13. cortex/metal/gpu_validator.py +177 -0
  14. cortex/metal/memory_pool.py +886 -0
  15. cortex/metal/mlx_accelerator.py +678 -0
  16. cortex/metal/mlx_converter.py +638 -0
  17. cortex/metal/mps_optimizer.py +417 -0
  18. cortex/metal/optimizer.py +665 -0
  19. cortex/metal/performance_profiler.py +364 -0
  20. cortex/model_downloader.py +130 -0
  21. cortex/model_manager.py +2187 -0
  22. cortex/quantization/__init__.py +5 -0
  23. cortex/quantization/dynamic_quantizer.py +736 -0
  24. cortex/template_registry/__init__.py +15 -0
  25. cortex/template_registry/auto_detector.py +144 -0
  26. cortex/template_registry/config_manager.py +234 -0
  27. cortex/template_registry/interactive.py +260 -0
  28. cortex/template_registry/registry.py +347 -0
  29. cortex/template_registry/template_profiles/__init__.py +5 -0
  30. cortex/template_registry/template_profiles/base.py +142 -0
  31. cortex/template_registry/template_profiles/complex/__init__.py +5 -0
  32. cortex/template_registry/template_profiles/complex/reasoning.py +263 -0
  33. cortex/template_registry/template_profiles/standard/__init__.py +9 -0
  34. cortex/template_registry/template_profiles/standard/alpaca.py +73 -0
  35. cortex/template_registry/template_profiles/standard/chatml.py +82 -0
  36. cortex/template_registry/template_profiles/standard/gemma.py +103 -0
  37. cortex/template_registry/template_profiles/standard/llama.py +87 -0
  38. cortex/template_registry/template_profiles/standard/simple.py +65 -0
  39. cortex/ui/__init__.py +120 -0
  40. cortex/ui/cli.py +1685 -0
  41. cortex/ui/markdown_render.py +185 -0
  42. cortex/ui/terminal_app.py +534 -0
  43. cortex_llm-1.0.0.dist-info/METADATA +275 -0
  44. cortex_llm-1.0.0.dist-info/RECORD +48 -0
  45. cortex_llm-1.0.0.dist-info/WHEEL +5 -0
  46. cortex_llm-1.0.0.dist-info/entry_points.txt +2 -0
  47. cortex_llm-1.0.0.dist-info/licenses/LICENSE +21 -0
  48. cortex_llm-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,468 @@
1
+ """Conversation management for Cortex."""
2
+
3
+ import json
4
+ import sqlite3
5
+ from pathlib import Path
6
+ from typing import Dict, Any, Optional, List, Tuple
7
+ from dataclasses import dataclass, asdict
8
+ from datetime import datetime
9
+ from enum import Enum
10
+ import hashlib
11
+ import uuid
12
+
13
+ class MessageRole(Enum):
14
+ """Role of message sender."""
15
+ SYSTEM = "system"
16
+ USER = "user"
17
+ ASSISTANT = "assistant"
18
+
19
+ @dataclass
20
+ class Message:
21
+ """A single message in a conversation."""
22
+ role: MessageRole
23
+ content: str
24
+ timestamp: datetime
25
+ tokens: Optional[int] = None
26
+ metadata: Optional[Dict[str, Any]] = None
27
+ message_id: Optional[str] = None
28
+
29
+ def __post_init__(self):
30
+ if self.message_id is None:
31
+ self.message_id = str(uuid.uuid4())
32
+ if self.metadata is None:
33
+ self.metadata = {}
34
+
35
+ def to_dict(self) -> Dict[str, Any]:
36
+ """Convert to dictionary."""
37
+ return {
38
+ 'role': self.role.value,
39
+ 'content': self.content,
40
+ 'timestamp': self.timestamp.isoformat(),
41
+ 'tokens': self.tokens,
42
+ 'metadata': self.metadata,
43
+ 'message_id': self.message_id
44
+ }
45
+
46
+ @classmethod
47
+ def from_dict(cls, data: Dict[str, Any]) -> 'Message':
48
+ """Create from dictionary."""
49
+ return cls(
50
+ role=MessageRole(data['role']),
51
+ content=data['content'],
52
+ timestamp=datetime.fromisoformat(data['timestamp']),
53
+ tokens=data.get('tokens'),
54
+ metadata=data.get('metadata', {}),
55
+ message_id=data.get('message_id')
56
+ )
57
+
58
+ @dataclass
59
+ class Conversation:
60
+ """A conversation thread."""
61
+ conversation_id: str
62
+ title: Optional[str]
63
+ created_at: datetime
64
+ updated_at: datetime
65
+ messages: List[Message]
66
+ metadata: Dict[str, Any]
67
+ parent_id: Optional[str] = None
68
+
69
+ def __post_init__(self):
70
+ if not self.conversation_id:
71
+ self.conversation_id = str(uuid.uuid4())
72
+ if self.metadata is None:
73
+ self.metadata = {}
74
+
75
+ def add_message(self, role: MessageRole, content: str, **kwargs) -> Message:
76
+ """Add a message to the conversation."""
77
+ message = Message(
78
+ role=role,
79
+ content=content,
80
+ timestamp=datetime.now(),
81
+ **kwargs
82
+ )
83
+ self.messages.append(message)
84
+ self.updated_at = datetime.now()
85
+ return message
86
+
87
+ def get_context(self, max_tokens: Optional[int] = None) -> List[Message]:
88
+ """Get conversation context within token limit."""
89
+ if max_tokens is None:
90
+ return self.messages
91
+
92
+ total_tokens = 0
93
+ context = []
94
+
95
+ for message in reversed(self.messages):
96
+ message_tokens = message.tokens or len(message.content.split()) * 1.3
97
+ if total_tokens + message_tokens > max_tokens:
98
+ break
99
+ context.insert(0, message)
100
+ total_tokens += message_tokens
101
+
102
+ return context
103
+
104
+ def branch(self, from_message_id: str) -> 'Conversation':
105
+ """Create a branch from a specific message."""
106
+ branch_point = None
107
+ for i, msg in enumerate(self.messages):
108
+ if msg.message_id == from_message_id:
109
+ branch_point = i
110
+ break
111
+
112
+ if branch_point is None:
113
+ raise ValueError(f"Message {from_message_id} not found")
114
+
115
+ return Conversation(
116
+ conversation_id=str(uuid.uuid4()),
117
+ title=f"{self.title} (branch)" if self.title else None,
118
+ created_at=datetime.now(),
119
+ updated_at=datetime.now(),
120
+ messages=self.messages[:branch_point + 1].copy(),
121
+ metadata={'branched_from': self.conversation_id},
122
+ parent_id=self.conversation_id
123
+ )
124
+
125
+ def to_dict(self) -> Dict[str, Any]:
126
+ """Convert to dictionary."""
127
+ return {
128
+ 'conversation_id': self.conversation_id,
129
+ 'title': self.title,
130
+ 'created_at': self.created_at.isoformat(),
131
+ 'updated_at': self.updated_at.isoformat(),
132
+ 'messages': [msg.to_dict() for msg in self.messages],
133
+ 'metadata': self.metadata,
134
+ 'parent_id': self.parent_id
135
+ }
136
+
137
+ @classmethod
138
+ def from_dict(cls, data: Dict[str, Any]) -> 'Conversation':
139
+ """Create from dictionary."""
140
+ return cls(
141
+ conversation_id=data['conversation_id'],
142
+ title=data.get('title'),
143
+ created_at=datetime.fromisoformat(data['created_at']),
144
+ updated_at=datetime.fromisoformat(data['updated_at']),
145
+ messages=[Message.from_dict(msg) for msg in data['messages']],
146
+ metadata=data.get('metadata', {}),
147
+ parent_id=data.get('parent_id')
148
+ )
149
+
150
+ def to_markdown(self) -> str:
151
+ """Export conversation to Markdown format."""
152
+ lines = []
153
+
154
+ if self.title:
155
+ lines.append(f"# {self.title}")
156
+ lines.append("")
157
+
158
+ lines.append(f"*Created: {self.created_at.strftime('%Y-%m-%d %H:%M:%S')}*")
159
+ lines.append(f"*Updated: {self.updated_at.strftime('%Y-%m-%d %H:%M:%S')}*")
160
+ lines.append("")
161
+ lines.append("---")
162
+ lines.append("")
163
+
164
+ for message in self.messages:
165
+ role_header = {
166
+ MessageRole.SYSTEM: "### System",
167
+ MessageRole.USER: "### User",
168
+ MessageRole.ASSISTANT: "### Assistant"
169
+ }[message.role]
170
+
171
+ lines.append(role_header)
172
+ lines.append("")
173
+ lines.append(message.content)
174
+ lines.append("")
175
+
176
+ return "\n".join(lines)
177
+
178
+ class ConversationManager:
179
+ """Manage conversations and persistence."""
180
+
181
+ def __init__(self, config: Any):
182
+ """Initialize conversation manager."""
183
+ self.config = config
184
+ self.conversations: Dict[str, Conversation] = {}
185
+ self.current_conversation_id: Optional[str] = None
186
+
187
+ self.save_dir = Path(config.conversation.save_directory)
188
+ self.save_dir.mkdir(parents=True, exist_ok=True)
189
+
190
+ if config.conversation.auto_save:
191
+ self._init_database()
192
+
193
+ self._load_recent_conversations()
194
+
195
+ def _init_database(self) -> None:
196
+ """Initialize SQLite database for persistence."""
197
+ self.db_path = self.save_dir / "conversations.db"
198
+
199
+ with sqlite3.connect(self.db_path) as conn:
200
+ conn.execute("""
201
+ CREATE TABLE IF NOT EXISTS conversations (
202
+ conversation_id TEXT PRIMARY KEY,
203
+ title TEXT,
204
+ created_at TEXT,
205
+ updated_at TEXT,
206
+ parent_id TEXT,
207
+ metadata TEXT,
208
+ FOREIGN KEY (parent_id) REFERENCES conversations(conversation_id)
209
+ )
210
+ """)
211
+
212
+ conn.execute("""
213
+ CREATE TABLE IF NOT EXISTS messages (
214
+ message_id TEXT PRIMARY KEY,
215
+ conversation_id TEXT,
216
+ role TEXT,
217
+ content TEXT,
218
+ timestamp TEXT,
219
+ tokens INTEGER,
220
+ metadata TEXT,
221
+ FOREIGN KEY (conversation_id) REFERENCES conversations(conversation_id)
222
+ )
223
+ """)
224
+
225
+ conn.execute("""
226
+ CREATE INDEX IF NOT EXISTS idx_messages_conversation
227
+ ON messages(conversation_id)
228
+ """)
229
+
230
+ def _load_recent_conversations(self) -> None:
231
+ """Load recent conversations from storage."""
232
+ if not hasattr(self, 'db_path') or not self.db_path.exists():
233
+ return
234
+
235
+ try:
236
+ with sqlite3.connect(self.db_path) as conn:
237
+ cursor = conn.execute("""
238
+ SELECT conversation_id, title, created_at, updated_at, parent_id, metadata
239
+ FROM conversations
240
+ ORDER BY updated_at DESC
241
+ LIMIT ?
242
+ """, (self.config.conversation.max_conversation_history,))
243
+
244
+ for row in cursor:
245
+ conv_id = row[0]
246
+
247
+ messages_cursor = conn.execute("""
248
+ SELECT role, content, timestamp, tokens, metadata, message_id
249
+ FROM messages
250
+ WHERE conversation_id = ?
251
+ ORDER BY timestamp ASC
252
+ """, (conv_id,))
253
+
254
+ messages = []
255
+ for msg_row in messages_cursor:
256
+ messages.append(Message(
257
+ role=MessageRole(msg_row[0]),
258
+ content=msg_row[1],
259
+ timestamp=datetime.fromisoformat(msg_row[2]),
260
+ tokens=msg_row[3],
261
+ metadata=json.loads(msg_row[4]) if msg_row[4] else {},
262
+ message_id=msg_row[5]
263
+ ))
264
+
265
+ conversation = Conversation(
266
+ conversation_id=conv_id,
267
+ title=row[1],
268
+ created_at=datetime.fromisoformat(row[2]),
269
+ updated_at=datetime.fromisoformat(row[3]),
270
+ messages=messages,
271
+ metadata=json.loads(row[5]) if row[5] else {},
272
+ parent_id=row[4]
273
+ )
274
+
275
+ self.conversations[conv_id] = conversation
276
+
277
+ except Exception as e:
278
+ print(f"Warning: Failed to load conversations: {e}")
279
+
280
+ def new_conversation(self, title: Optional[str] = None) -> Conversation:
281
+ """Create a new conversation."""
282
+ conversation = Conversation(
283
+ conversation_id=str(uuid.uuid4()),
284
+ title=title,
285
+ created_at=datetime.now(),
286
+ updated_at=datetime.now(),
287
+ messages=[],
288
+ metadata={}
289
+ )
290
+
291
+ self.conversations[conversation.conversation_id] = conversation
292
+ self.current_conversation_id = conversation.conversation_id
293
+
294
+ if self.config.conversation.auto_save:
295
+ self._save_conversation(conversation)
296
+
297
+ return conversation
298
+
299
+ def get_current_conversation(self) -> Optional[Conversation]:
300
+ """Get the current active conversation."""
301
+ if self.current_conversation_id:
302
+ return self.conversations.get(self.current_conversation_id)
303
+ return None
304
+
305
+ def switch_conversation(self, conversation_id: str) -> bool:
306
+ """Switch to a different conversation."""
307
+ if conversation_id in self.conversations:
308
+ self.current_conversation_id = conversation_id
309
+ return True
310
+ return False
311
+
312
+ def add_message(
313
+ self,
314
+ role: MessageRole,
315
+ content: str,
316
+ conversation_id: Optional[str] = None,
317
+ **kwargs
318
+ ) -> Message:
319
+ """Add a message to a conversation."""
320
+ conv_id = conversation_id or self.current_conversation_id
321
+
322
+ if not conv_id:
323
+ conversation = self.new_conversation()
324
+ conv_id = conversation.conversation_id
325
+
326
+ conversation = self.conversations.get(conv_id)
327
+ if not conversation:
328
+ raise ValueError(f"Conversation {conv_id} not found")
329
+
330
+ message = conversation.add_message(role, content, **kwargs)
331
+
332
+ if self.config.conversation.auto_save:
333
+ self._save_message(conv_id, message)
334
+
335
+ return message
336
+
337
+ def _save_conversation(self, conversation: Conversation) -> None:
338
+ """Save conversation to database."""
339
+ if not hasattr(self, 'db_path'):
340
+ return
341
+
342
+ try:
343
+ with sqlite3.connect(self.db_path) as conn:
344
+ conn.execute("""
345
+ INSERT OR REPLACE INTO conversations
346
+ (conversation_id, title, created_at, updated_at, parent_id, metadata)
347
+ VALUES (?, ?, ?, ?, ?, ?)
348
+ """, (
349
+ conversation.conversation_id,
350
+ conversation.title,
351
+ conversation.created_at.isoformat(),
352
+ conversation.updated_at.isoformat(),
353
+ conversation.parent_id,
354
+ json.dumps(conversation.metadata)
355
+ ))
356
+ except Exception as e:
357
+ print(f"Warning: Failed to save conversation: {e}")
358
+
359
+ def _save_message(self, conversation_id: str, message: Message) -> None:
360
+ """Save message to database."""
361
+ if not hasattr(self, 'db_path'):
362
+ return
363
+
364
+ try:
365
+ with sqlite3.connect(self.db_path) as conn:
366
+ conn.execute("""
367
+ INSERT OR REPLACE INTO messages
368
+ (message_id, conversation_id, role, content, timestamp, tokens, metadata)
369
+ VALUES (?, ?, ?, ?, ?, ?, ?)
370
+ """, (
371
+ message.message_id,
372
+ conversation_id,
373
+ message.role.value,
374
+ message.content,
375
+ message.timestamp.isoformat(),
376
+ message.tokens,
377
+ json.dumps(message.metadata)
378
+ ))
379
+ except Exception as e:
380
+ print(f"Warning: Failed to save message: {e}")
381
+
382
+ def export_conversation(
383
+ self,
384
+ conversation_id: Optional[str] = None,
385
+ format: str = "json"
386
+ ) -> str:
387
+ """Export conversation to specified format."""
388
+ conv_id = conversation_id or self.current_conversation_id
389
+
390
+ if not conv_id or conv_id not in self.conversations:
391
+ raise ValueError("No conversation to export")
392
+
393
+ conversation = self.conversations[conv_id]
394
+
395
+ if format == "json":
396
+ return json.dumps(conversation.to_dict(), indent=2)
397
+ elif format == "markdown":
398
+ return conversation.to_markdown()
399
+ else:
400
+ raise ValueError(f"Unsupported format: {format}")
401
+
402
+ def import_conversation(self, data: str, format: str = "json") -> Conversation:
403
+ """Import conversation from string."""
404
+ if format == "json":
405
+ conv_dict = json.loads(data)
406
+ conversation = Conversation.from_dict(conv_dict)
407
+ else:
408
+ raise ValueError(f"Unsupported format: {format}")
409
+
410
+ self.conversations[conversation.conversation_id] = conversation
411
+
412
+ if self.config.conversation.auto_save:
413
+ self._save_conversation(conversation)
414
+ for message in conversation.messages:
415
+ self._save_message(conversation.conversation_id, message)
416
+
417
+ return conversation
418
+
419
+ def list_conversations(self) -> List[Dict[str, Any]]:
420
+ """List all conversations."""
421
+ return [
422
+ {
423
+ 'id': conv.conversation_id,
424
+ 'title': conv.title or f"Conversation {conv.created_at.strftime('%Y-%m-%d %H:%M')}",
425
+ 'created_at': conv.created_at.isoformat(),
426
+ 'updated_at': conv.updated_at.isoformat(),
427
+ 'message_count': len(conv.messages),
428
+ 'is_current': conv.conversation_id == self.current_conversation_id
429
+ }
430
+ for conv in sorted(
431
+ self.conversations.values(),
432
+ key=lambda x: x.updated_at,
433
+ reverse=True
434
+ )
435
+ ]
436
+
437
+ def delete_conversation(self, conversation_id: str) -> bool:
438
+ """Delete a conversation."""
439
+ if conversation_id not in self.conversations:
440
+ return False
441
+
442
+ del self.conversations[conversation_id]
443
+
444
+ if self.current_conversation_id == conversation_id:
445
+ self.current_conversation_id = None
446
+
447
+ if hasattr(self, 'db_path'):
448
+ try:
449
+ with sqlite3.connect(self.db_path) as conn:
450
+ conn.execute("DELETE FROM messages WHERE conversation_id = ?", (conversation_id,))
451
+ conn.execute("DELETE FROM conversations WHERE conversation_id = ?", (conversation_id,))
452
+ except Exception as e:
453
+ print(f"Warning: Failed to delete conversation from database: {e}")
454
+
455
+ return True
456
+
457
+ def clear_all(self) -> None:
458
+ """Clear all conversations."""
459
+ self.conversations.clear()
460
+ self.current_conversation_id = None
461
+
462
+ if hasattr(self, 'db_path'):
463
+ try:
464
+ with sqlite3.connect(self.db_path) as conn:
465
+ conn.execute("DELETE FROM messages")
466
+ conn.execute("DELETE FROM conversations")
467
+ except Exception as e:
468
+ print(f"Warning: Failed to clear database: {e}")
@@ -0,0 +1,8 @@
1
+ """Fine-tuning module for Cortex."""
2
+
3
+ from .wizard import FineTuneWizard
4
+ from .trainer import LoRATrainer
5
+ from .dataset import DatasetPreparer
6
+ from .mlx_lora_trainer import MLXLoRATrainer
7
+
8
+ __all__ = ['FineTuneWizard', 'LoRATrainer', 'DatasetPreparer', 'MLXLoRATrainer']