ai-coding-assistant 0.5.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 (89) hide show
  1. ai_coding_assistant-0.5.0.dist-info/METADATA +226 -0
  2. ai_coding_assistant-0.5.0.dist-info/RECORD +89 -0
  3. ai_coding_assistant-0.5.0.dist-info/WHEEL +4 -0
  4. ai_coding_assistant-0.5.0.dist-info/entry_points.txt +3 -0
  5. ai_coding_assistant-0.5.0.dist-info/licenses/LICENSE +21 -0
  6. coding_assistant/__init__.py +3 -0
  7. coding_assistant/__main__.py +19 -0
  8. coding_assistant/cli/__init__.py +1 -0
  9. coding_assistant/cli/app.py +158 -0
  10. coding_assistant/cli/commands/__init__.py +19 -0
  11. coding_assistant/cli/commands/ask.py +178 -0
  12. coding_assistant/cli/commands/config.py +438 -0
  13. coding_assistant/cli/commands/diagram.py +267 -0
  14. coding_assistant/cli/commands/document.py +410 -0
  15. coding_assistant/cli/commands/explain.py +192 -0
  16. coding_assistant/cli/commands/fix.py +249 -0
  17. coding_assistant/cli/commands/index.py +162 -0
  18. coding_assistant/cli/commands/refactor.py +245 -0
  19. coding_assistant/cli/commands/search.py +182 -0
  20. coding_assistant/cli/commands/serve_docs.py +128 -0
  21. coding_assistant/cli/repl.py +381 -0
  22. coding_assistant/cli/theme.py +90 -0
  23. coding_assistant/codebase/__init__.py +1 -0
  24. coding_assistant/codebase/crawler.py +93 -0
  25. coding_assistant/codebase/parser.py +266 -0
  26. coding_assistant/config/__init__.py +25 -0
  27. coding_assistant/config/config_manager.py +615 -0
  28. coding_assistant/config/settings.py +82 -0
  29. coding_assistant/context/__init__.py +19 -0
  30. coding_assistant/context/chunker.py +443 -0
  31. coding_assistant/context/enhanced_retriever.py +322 -0
  32. coding_assistant/context/hybrid_search.py +311 -0
  33. coding_assistant/context/ranker.py +355 -0
  34. coding_assistant/context/retriever.py +119 -0
  35. coding_assistant/context/window.py +362 -0
  36. coding_assistant/documentation/__init__.py +23 -0
  37. coding_assistant/documentation/agents/__init__.py +27 -0
  38. coding_assistant/documentation/agents/coordinator.py +510 -0
  39. coding_assistant/documentation/agents/module_documenter.py +111 -0
  40. coding_assistant/documentation/agents/synthesizer.py +139 -0
  41. coding_assistant/documentation/agents/task_delegator.py +100 -0
  42. coding_assistant/documentation/decomposition/__init__.py +21 -0
  43. coding_assistant/documentation/decomposition/context_preserver.py +477 -0
  44. coding_assistant/documentation/decomposition/module_detector.py +302 -0
  45. coding_assistant/documentation/decomposition/partitioner.py +621 -0
  46. coding_assistant/documentation/generators/__init__.py +14 -0
  47. coding_assistant/documentation/generators/dataflow_generator.py +440 -0
  48. coding_assistant/documentation/generators/diagram_generator.py +511 -0
  49. coding_assistant/documentation/graph/__init__.py +13 -0
  50. coding_assistant/documentation/graph/dependency_builder.py +468 -0
  51. coding_assistant/documentation/graph/module_analyzer.py +475 -0
  52. coding_assistant/documentation/writers/__init__.py +11 -0
  53. coding_assistant/documentation/writers/markdown_writer.py +322 -0
  54. coding_assistant/embeddings/__init__.py +0 -0
  55. coding_assistant/embeddings/generator.py +89 -0
  56. coding_assistant/embeddings/store.py +187 -0
  57. coding_assistant/exceptions/__init__.py +50 -0
  58. coding_assistant/exceptions/base.py +110 -0
  59. coding_assistant/exceptions/llm.py +249 -0
  60. coding_assistant/exceptions/recovery.py +263 -0
  61. coding_assistant/exceptions/storage.py +213 -0
  62. coding_assistant/exceptions/validation.py +230 -0
  63. coding_assistant/llm/__init__.py +1 -0
  64. coding_assistant/llm/client.py +277 -0
  65. coding_assistant/llm/gemini_client.py +181 -0
  66. coding_assistant/llm/groq_client.py +160 -0
  67. coding_assistant/llm/prompts.py +98 -0
  68. coding_assistant/llm/together_client.py +160 -0
  69. coding_assistant/operations/__init__.py +13 -0
  70. coding_assistant/operations/differ.py +369 -0
  71. coding_assistant/operations/generator.py +347 -0
  72. coding_assistant/operations/linter.py +430 -0
  73. coding_assistant/operations/validator.py +406 -0
  74. coding_assistant/storage/__init__.py +9 -0
  75. coding_assistant/storage/database.py +363 -0
  76. coding_assistant/storage/session.py +231 -0
  77. coding_assistant/utils/__init__.py +31 -0
  78. coding_assistant/utils/cache.py +477 -0
  79. coding_assistant/utils/hardware.py +132 -0
  80. coding_assistant/utils/keystore.py +206 -0
  81. coding_assistant/utils/logger.py +32 -0
  82. coding_assistant/utils/progress.py +311 -0
  83. coding_assistant/validation/__init__.py +13 -0
  84. coding_assistant/validation/files.py +305 -0
  85. coding_assistant/validation/inputs.py +335 -0
  86. coding_assistant/validation/params.py +280 -0
  87. coding_assistant/validation/sanitizers.py +243 -0
  88. coding_assistant/vcs/__init__.py +5 -0
  89. coding_assistant/vcs/git.py +269 -0
@@ -0,0 +1,363 @@
1
+ """Database layer for session persistence."""
2
+
3
+ import sqlite3
4
+ import json
5
+ from pathlib import Path
6
+ from typing import List, Dict, Optional, Any
7
+ from datetime import datetime
8
+ from contextlib import contextmanager
9
+
10
+
11
+ class Database:
12
+ """SQLite database for storing sessions, messages, and code changes."""
13
+
14
+ def __init__(self, db_path: str):
15
+ """Initialize database.
16
+
17
+ Args:
18
+ db_path: Path to SQLite database file
19
+ """
20
+ self.db_path = Path(db_path)
21
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
22
+ self._init_schema()
23
+
24
+ @contextmanager
25
+ def get_connection(self):
26
+ """Get database connection context manager."""
27
+ conn = sqlite3.connect(str(self.db_path))
28
+ conn.row_factory = sqlite3.Row # Return rows as dicts
29
+ try:
30
+ yield conn
31
+ conn.commit()
32
+ except Exception:
33
+ conn.rollback()
34
+ raise
35
+ finally:
36
+ conn.close()
37
+
38
+ def _init_schema(self):
39
+ """Initialize database schema."""
40
+ with self.get_connection() as conn:
41
+ cursor = conn.cursor()
42
+
43
+ # Sessions table
44
+ cursor.execute("""
45
+ CREATE TABLE IF NOT EXISTS sessions (
46
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
47
+ project_path TEXT NOT NULL,
48
+ started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
49
+ ended_at TIMESTAMP,
50
+ title TEXT,
51
+ metadata TEXT
52
+ )
53
+ """)
54
+
55
+ # Messages table
56
+ cursor.execute("""
57
+ CREATE TABLE IF NOT EXISTS messages (
58
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
59
+ session_id INTEGER NOT NULL,
60
+ role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system')),
61
+ content TEXT NOT NULL,
62
+ tokens INTEGER,
63
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
64
+ metadata TEXT,
65
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
66
+ )
67
+ """)
68
+
69
+ # Code changes table
70
+ cursor.execute("""
71
+ CREATE TABLE IF NOT EXISTS code_changes (
72
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
73
+ session_id INTEGER NOT NULL,
74
+ message_id INTEGER,
75
+ file_path TEXT NOT NULL,
76
+ change_type TEXT CHECK(change_type IN ('create', 'modify', 'delete')),
77
+ original_content TEXT,
78
+ new_content TEXT,
79
+ diff TEXT,
80
+ applied BOOLEAN DEFAULT 0,
81
+ applied_at TIMESTAMP,
82
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
83
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
84
+ FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE SET NULL
85
+ )
86
+ """)
87
+
88
+ # Context snapshots table
89
+ cursor.execute("""
90
+ CREATE TABLE IF NOT EXISTS context_snapshots (
91
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
92
+ session_id INTEGER NOT NULL,
93
+ message_id INTEGER NOT NULL,
94
+ retrieved_chunks TEXT,
95
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
96
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
97
+ FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE
98
+ )
99
+ """)
100
+
101
+ # Create indexes
102
+ cursor.execute("""
103
+ CREATE INDEX IF NOT EXISTS idx_messages_session
104
+ ON messages(session_id)
105
+ """)
106
+
107
+ cursor.execute("""
108
+ CREATE INDEX IF NOT EXISTS idx_code_changes_session
109
+ ON code_changes(session_id)
110
+ """)
111
+
112
+ cursor.execute("""
113
+ CREATE INDEX IF NOT EXISTS idx_sessions_project
114
+ ON sessions(project_path)
115
+ """)
116
+
117
+ # Session operations
118
+
119
+ def insert_session(self, project_path: str, title: Optional[str] = None,
120
+ metadata: Optional[Dict] = None) -> int:
121
+ """Create a new session.
122
+
123
+ Args:
124
+ project_path: Path to project
125
+ title: Optional session title
126
+ metadata: Optional metadata dict
127
+
128
+ Returns:
129
+ Session ID
130
+ """
131
+ with self.get_connection() as conn:
132
+ cursor = conn.cursor()
133
+ cursor.execute("""
134
+ INSERT INTO sessions (project_path, title, metadata)
135
+ VALUES (?, ?, ?)
136
+ """, (project_path, title, json.dumps(metadata) if metadata else None))
137
+ return cursor.lastrowid
138
+
139
+ def update_session(self, session_id: int, ended_at: Optional[datetime] = None,
140
+ title: Optional[str] = None, metadata: Optional[Dict] = None):
141
+ """Update session.
142
+
143
+ Args:
144
+ session_id: Session ID
145
+ ended_at: End timestamp
146
+ title: New title
147
+ metadata: New metadata
148
+ """
149
+ updates = []
150
+ params = []
151
+
152
+ if ended_at:
153
+ updates.append("ended_at = ?")
154
+ params.append(ended_at.isoformat())
155
+
156
+ if title is not None:
157
+ updates.append("title = ?")
158
+ params.append(title)
159
+
160
+ if metadata is not None:
161
+ updates.append("metadata = ?")
162
+ params.append(json.dumps(metadata))
163
+
164
+ if not updates:
165
+ return
166
+
167
+ params.append(session_id)
168
+ query = f"UPDATE sessions SET {', '.join(updates)} WHERE id = ?"
169
+
170
+ with self.get_connection() as conn:
171
+ conn.execute(query, params)
172
+
173
+ def get_session(self, session_id: int) -> Optional[Dict]:
174
+ """Get session by ID.
175
+
176
+ Args:
177
+ session_id: Session ID
178
+
179
+ Returns:
180
+ Session dict or None
181
+ """
182
+ with self.get_connection() as conn:
183
+ cursor = conn.cursor()
184
+ cursor.execute("SELECT * FROM sessions WHERE id = ?", (session_id,))
185
+ row = cursor.fetchone()
186
+ if row:
187
+ return dict(row)
188
+ return None
189
+
190
+ def get_recent_sessions(self, limit: int = 10, project_path: Optional[str] = None) -> List[Dict]:
191
+ """Get recent sessions.
192
+
193
+ Args:
194
+ limit: Maximum number of sessions
195
+ project_path: Filter by project path
196
+
197
+ Returns:
198
+ List of session dicts
199
+ """
200
+ with self.get_connection() as conn:
201
+ cursor = conn.cursor()
202
+
203
+ if project_path:
204
+ cursor.execute("""
205
+ SELECT * FROM sessions
206
+ WHERE project_path = ?
207
+ ORDER BY started_at DESC
208
+ LIMIT ?
209
+ """, (project_path, limit))
210
+ else:
211
+ cursor.execute("""
212
+ SELECT * FROM sessions
213
+ ORDER BY started_at DESC
214
+ LIMIT ?
215
+ """, (limit,))
216
+
217
+ return [dict(row) for row in cursor.fetchall()]
218
+
219
+ # Message operations
220
+
221
+ def insert_message(self, session_id: int, role: str, content: str,
222
+ tokens: Optional[int] = None, metadata: Optional[Dict] = None) -> int:
223
+ """Add message to session.
224
+
225
+ Args:
226
+ session_id: Session ID
227
+ role: Message role (user/assistant/system)
228
+ content: Message content
229
+ tokens: Token count
230
+ metadata: Optional metadata
231
+
232
+ Returns:
233
+ Message ID
234
+ """
235
+ with self.get_connection() as conn:
236
+ cursor = conn.cursor()
237
+ cursor.execute("""
238
+ INSERT INTO messages (session_id, role, content, tokens, metadata)
239
+ VALUES (?, ?, ?, ?, ?)
240
+ """, (session_id, role, content, tokens, json.dumps(metadata) if metadata else None))
241
+ return cursor.lastrowid
242
+
243
+ def get_messages_by_session(self, session_id: int) -> List[Dict]:
244
+ """Get all messages in a session.
245
+
246
+ Args:
247
+ session_id: Session ID
248
+
249
+ Returns:
250
+ List of message dicts
251
+ """
252
+ with self.get_connection() as conn:
253
+ cursor = conn.cursor()
254
+ cursor.execute("""
255
+ SELECT * FROM messages
256
+ WHERE session_id = ?
257
+ ORDER BY created_at ASC
258
+ """, (session_id,))
259
+ return [dict(row) for row in cursor.fetchall()]
260
+
261
+ # Code change operations
262
+
263
+ def insert_code_change(self, session_id: int, file_path: str,
264
+ change_type: str, original_content: Optional[str] = None,
265
+ new_content: Optional[str] = None, diff: Optional[str] = None,
266
+ message_id: Optional[int] = None) -> int:
267
+ """Record a code change.
268
+
269
+ Args:
270
+ session_id: Session ID
271
+ file_path: Path to changed file
272
+ change_type: Type of change (create/modify/delete)
273
+ original_content: Original file content
274
+ new_content: New file content
275
+ diff: Diff text
276
+ message_id: Associated message ID
277
+
278
+ Returns:
279
+ Code change ID
280
+ """
281
+ with self.get_connection() as conn:
282
+ cursor = conn.cursor()
283
+ cursor.execute("""
284
+ INSERT INTO code_changes
285
+ (session_id, message_id, file_path, change_type,
286
+ original_content, new_content, diff)
287
+ VALUES (?, ?, ?, ?, ?, ?, ?)
288
+ """, (session_id, message_id, file_path, change_type,
289
+ original_content, new_content, diff))
290
+ return cursor.lastrowid
291
+
292
+ def mark_change_applied(self, change_id: int):
293
+ """Mark a code change as applied.
294
+
295
+ Args:
296
+ change_id: Code change ID
297
+ """
298
+ with self.get_connection() as conn:
299
+ conn.execute("""
300
+ UPDATE code_changes
301
+ SET applied = 1, applied_at = CURRENT_TIMESTAMP
302
+ WHERE id = ?
303
+ """, (change_id,))
304
+
305
+ def get_code_changes(self, session_id: int) -> List[Dict]:
306
+ """Get all code changes for a session.
307
+
308
+ Args:
309
+ session_id: Session ID
310
+
311
+ Returns:
312
+ List of code change dicts
313
+ """
314
+ with self.get_connection() as conn:
315
+ cursor = conn.cursor()
316
+ cursor.execute("""
317
+ SELECT * FROM code_changes
318
+ WHERE session_id = ?
319
+ ORDER BY created_at ASC
320
+ """, (session_id,))
321
+ return [dict(row) for row in cursor.fetchall()]
322
+
323
+ # Context snapshot operations
324
+
325
+ def insert_context_snapshot(self, session_id: int, message_id: int,
326
+ retrieved_chunks: List[Dict]) -> int:
327
+ """Save a context snapshot.
328
+
329
+ Args:
330
+ session_id: Session ID
331
+ message_id: Message ID
332
+ retrieved_chunks: List of retrieved chunks
333
+
334
+ Returns:
335
+ Snapshot ID
336
+ """
337
+ with self.get_connection() as conn:
338
+ cursor = conn.cursor()
339
+ cursor.execute("""
340
+ INSERT INTO context_snapshots (session_id, message_id, retrieved_chunks)
341
+ VALUES (?, ?, ?)
342
+ """, (session_id, message_id, json.dumps(retrieved_chunks)))
343
+ return cursor.lastrowid
344
+
345
+ def get_context_snapshot(self, message_id: int) -> Optional[List[Dict]]:
346
+ """Get context snapshot for a message.
347
+
348
+ Args:
349
+ message_id: Message ID
350
+
351
+ Returns:
352
+ List of chunks or None
353
+ """
354
+ with self.get_connection() as conn:
355
+ cursor = conn.cursor()
356
+ cursor.execute("""
357
+ SELECT retrieved_chunks FROM context_snapshots
358
+ WHERE message_id = ?
359
+ """, (message_id,))
360
+ row = cursor.fetchone()
361
+ if row:
362
+ return json.loads(row['retrieved_chunks'])
363
+ return None
@@ -0,0 +1,231 @@
1
+ """Session management for persistent conversations."""
2
+
3
+ from typing import List, Dict, Optional
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+
7
+ from coding_assistant.storage.database import Database
8
+ from coding_assistant.operations.differ import DiffGenerator
9
+
10
+
11
+ class SessionManager:
12
+ """Manages conversation sessions with persistence."""
13
+
14
+ def __init__(self, db_path: str):
15
+ """Initialize session manager.
16
+
17
+ Args:
18
+ db_path: Path to SQLite database
19
+ """
20
+ self.db = Database(db_path)
21
+ self.diff_generator = DiffGenerator()
22
+
23
+ def create_session(self, project_path: str, title: Optional[str] = None) -> int:
24
+ """Create a new session.
25
+
26
+ Args:
27
+ project_path: Path to project
28
+ title: Optional session title
29
+
30
+ Returns:
31
+ Session ID
32
+ """
33
+ return self.db.insert_session(project_path, title)
34
+
35
+ def end_session(self, session_id: int):
36
+ """Mark session as ended.
37
+
38
+ Args:
39
+ session_id: Session ID
40
+ """
41
+ self.db.update_session(session_id, ended_at=datetime.now())
42
+
43
+ def update_session_title(self, session_id: int, title: str):
44
+ """Update session title.
45
+
46
+ Args:
47
+ session_id: Session ID
48
+ title: New title
49
+ """
50
+ self.db.update_session(session_id, title=title)
51
+
52
+ def add_message(self, session_id: int, role: str, content: str,
53
+ tokens: Optional[int] = None) -> int:
54
+ """Add message to session.
55
+
56
+ Args:
57
+ session_id: Session ID
58
+ role: Message role (user/assistant/system)
59
+ content: Message content
60
+ tokens: Optional token count
61
+
62
+ Returns:
63
+ Message ID
64
+ """
65
+ return self.db.insert_message(session_id, role, content, tokens)
66
+
67
+ def get_session_history(self, session_id: int) -> List[Dict]:
68
+ """Get all messages in a session.
69
+
70
+ Args:
71
+ session_id: Session ID
72
+
73
+ Returns:
74
+ List of messages
75
+ """
76
+ messages = self.db.get_messages_by_session(session_id)
77
+ # Convert to format expected by LLM
78
+ return [
79
+ {
80
+ 'role': msg['role'],
81
+ 'content': msg['content']
82
+ }
83
+ for msg in messages
84
+ ]
85
+
86
+ def record_code_change(self, session_id: int, file_path: str,
87
+ original_content: str, new_content: str,
88
+ message_id: Optional[int] = None) -> int:
89
+ """Record a code change.
90
+
91
+ Args:
92
+ session_id: Session ID
93
+ file_path: Path to file
94
+ original_content: Original content
95
+ new_content: New content
96
+ message_id: Associated message ID
97
+
98
+ Returns:
99
+ Code change ID
100
+ """
101
+ # Generate diff
102
+ diff = self.diff_generator.generate_diff(
103
+ original_content,
104
+ new_content,
105
+ file_path
106
+ )
107
+
108
+ # Determine change type
109
+ if not original_content:
110
+ change_type = 'create'
111
+ elif not new_content:
112
+ change_type = 'delete'
113
+ else:
114
+ change_type = 'modify'
115
+
116
+ return self.db.insert_code_change(
117
+ session_id=session_id,
118
+ file_path=file_path,
119
+ change_type=change_type,
120
+ original_content=original_content,
121
+ new_content=new_content,
122
+ diff=diff,
123
+ message_id=message_id
124
+ )
125
+
126
+ def mark_change_applied(self, change_id: int):
127
+ """Mark a code change as applied.
128
+
129
+ Args:
130
+ change_id: Code change ID
131
+ """
132
+ self.db.mark_change_applied(change_id)
133
+
134
+ def get_code_changes(self, session_id: int) -> List[Dict]:
135
+ """Get all code changes for a session.
136
+
137
+ Args:
138
+ session_id: Session ID
139
+
140
+ Returns:
141
+ List of code changes
142
+ """
143
+ return self.db.get_code_changes(session_id)
144
+
145
+ def save_context_snapshot(self, session_id: int, message_id: int,
146
+ retrieved_chunks: List[Dict]) -> int:
147
+ """Save context snapshot for a message.
148
+
149
+ Args:
150
+ session_id: Session ID
151
+ message_id: Message ID
152
+ retrieved_chunks: Retrieved context chunks
153
+
154
+ Returns:
155
+ Snapshot ID
156
+ """
157
+ return self.db.insert_context_snapshot(
158
+ session_id,
159
+ message_id,
160
+ retrieved_chunks
161
+ )
162
+
163
+ def get_context_snapshot(self, message_id: int) -> Optional[List[Dict]]:
164
+ """Get context snapshot for a message.
165
+
166
+ Args:
167
+ message_id: Message ID
168
+
169
+ Returns:
170
+ List of chunks or None
171
+ """
172
+ return self.db.get_context_snapshot(message_id)
173
+
174
+ def list_sessions(self, limit: int = 10, project_path: Optional[str] = None) -> List[Dict]:
175
+ """List recent sessions.
176
+
177
+ Args:
178
+ limit: Maximum number of sessions
179
+ project_path: Filter by project path
180
+
181
+ Returns:
182
+ List of sessions
183
+ """
184
+ return self.db.get_recent_sessions(limit, project_path)
185
+
186
+ def get_session(self, session_id: int) -> Optional[Dict]:
187
+ """Get session info.
188
+
189
+ Args:
190
+ session_id: Session ID
191
+
192
+ Returns:
193
+ Session dict or None
194
+ """
195
+ return self.db.get_session(session_id)
196
+
197
+ def get_session_summary(self, session_id: int) -> Dict:
198
+ """Get summary of a session.
199
+
200
+ Args:
201
+ session_id: Session ID
202
+
203
+ Returns:
204
+ Summary dict with message count, code changes, etc.
205
+ """
206
+ session = self.get_session(session_id)
207
+ if not session:
208
+ return {}
209
+
210
+ messages = self.db.get_messages_by_session(session_id)
211
+ code_changes = self.get_code_changes(session_id)
212
+
213
+ # Count by role
214
+ user_messages = sum(1 for m in messages if m['role'] == 'user')
215
+ assistant_messages = sum(1 for m in messages if m['role'] == 'assistant')
216
+
217
+ # Count applied changes
218
+ applied_changes = sum(1 for c in code_changes if c['applied'])
219
+
220
+ return {
221
+ 'session_id': session_id,
222
+ 'project_path': session['project_path'],
223
+ 'started_at': session['started_at'],
224
+ 'ended_at': session['ended_at'],
225
+ 'title': session['title'],
226
+ 'total_messages': len(messages),
227
+ 'user_messages': user_messages,
228
+ 'assistant_messages': assistant_messages,
229
+ 'code_changes': len(code_changes),
230
+ 'applied_changes': applied_changes,
231
+ }
@@ -0,0 +1,31 @@
1
+ """Utility functions and helpers."""
2
+
3
+ from coding_assistant.utils.cache import (
4
+ Cache,
5
+ EmbeddingCache,
6
+ ResponseCache,
7
+ CacheEntry,
8
+ compute_context_hash
9
+ )
10
+ from coding_assistant.utils.progress import (
11
+ ProgressManager,
12
+ ProgressCallback,
13
+ status,
14
+ progress_bar,
15
+ show_operation,
16
+ get_progress_manager
17
+ )
18
+
19
+ __all__ = [
20
+ 'Cache',
21
+ 'EmbeddingCache',
22
+ 'ResponseCache',
23
+ 'CacheEntry',
24
+ 'compute_context_hash',
25
+ 'ProgressManager',
26
+ 'ProgressCallback',
27
+ 'status',
28
+ 'progress_bar',
29
+ 'show_operation',
30
+ 'get_progress_manager',
31
+ ]