claude-mpm 5.4.96__py3-none-any.whl → 5.6.17__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 claude-mpm might be problematic. Click here for more details.

Files changed (214) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/{CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md → CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md} +14 -6
  3. claude_mpm/agents/PM_INSTRUCTIONS.md +44 -10
  4. claude_mpm/agents/WORKFLOW.md +2 -0
  5. claude_mpm/agents/templates/circuit-breakers.md +26 -17
  6. claude_mpm/cli/commands/autotodos.py +45 -5
  7. claude_mpm/cli/commands/commander.py +216 -0
  8. claude_mpm/cli/commands/hook_errors.py +60 -60
  9. claude_mpm/cli/commands/run.py +35 -3
  10. claude_mpm/cli/commands/skill_source.py +51 -2
  11. claude_mpm/cli/commands/skills.py +5 -3
  12. claude_mpm/cli/executor.py +32 -17
  13. claude_mpm/cli/parsers/base_parser.py +17 -0
  14. claude_mpm/cli/parsers/commander_parser.py +116 -0
  15. claude_mpm/cli/parsers/run_parser.py +10 -0
  16. claude_mpm/cli/parsers/skill_source_parser.py +4 -0
  17. claude_mpm/cli/parsers/skills_parser.py +5 -0
  18. claude_mpm/cli/startup.py +124 -3
  19. claude_mpm/cli/startup_display.py +2 -1
  20. claude_mpm/cli/utils.py +7 -3
  21. claude_mpm/commander/__init__.py +78 -0
  22. claude_mpm/commander/adapters/__init__.py +60 -0
  23. claude_mpm/commander/adapters/auggie.py +260 -0
  24. claude_mpm/commander/adapters/base.py +288 -0
  25. claude_mpm/commander/adapters/claude_code.py +392 -0
  26. claude_mpm/commander/adapters/codex.py +237 -0
  27. claude_mpm/commander/adapters/communication.py +366 -0
  28. claude_mpm/commander/adapters/example_usage.py +310 -0
  29. claude_mpm/commander/adapters/mpm.py +389 -0
  30. claude_mpm/commander/adapters/registry.py +204 -0
  31. claude_mpm/commander/api/__init__.py +16 -0
  32. claude_mpm/commander/api/app.py +121 -0
  33. claude_mpm/commander/api/errors.py +133 -0
  34. claude_mpm/commander/api/routes/__init__.py +8 -0
  35. claude_mpm/commander/api/routes/events.py +184 -0
  36. claude_mpm/commander/api/routes/inbox.py +171 -0
  37. claude_mpm/commander/api/routes/messages.py +148 -0
  38. claude_mpm/commander/api/routes/projects.py +271 -0
  39. claude_mpm/commander/api/routes/sessions.py +226 -0
  40. claude_mpm/commander/api/routes/work.py +296 -0
  41. claude_mpm/commander/api/schemas.py +186 -0
  42. claude_mpm/commander/chat/__init__.py +7 -0
  43. claude_mpm/commander/chat/cli.py +111 -0
  44. claude_mpm/commander/chat/commands.py +96 -0
  45. claude_mpm/commander/chat/repl.py +310 -0
  46. claude_mpm/commander/config.py +49 -0
  47. claude_mpm/commander/config_loader.py +115 -0
  48. claude_mpm/commander/core/__init__.py +10 -0
  49. claude_mpm/commander/core/block_manager.py +325 -0
  50. claude_mpm/commander/core/response_manager.py +323 -0
  51. claude_mpm/commander/daemon.py +594 -0
  52. claude_mpm/commander/env_loader.py +59 -0
  53. claude_mpm/commander/events/__init__.py +26 -0
  54. claude_mpm/commander/events/manager.py +332 -0
  55. claude_mpm/commander/frameworks/__init__.py +12 -0
  56. claude_mpm/commander/frameworks/base.py +143 -0
  57. claude_mpm/commander/frameworks/claude_code.py +58 -0
  58. claude_mpm/commander/frameworks/mpm.py +62 -0
  59. claude_mpm/commander/inbox/__init__.py +16 -0
  60. claude_mpm/commander/inbox/dedup.py +128 -0
  61. claude_mpm/commander/inbox/inbox.py +224 -0
  62. claude_mpm/commander/inbox/models.py +70 -0
  63. claude_mpm/commander/instance_manager.py +337 -0
  64. claude_mpm/commander/llm/__init__.py +6 -0
  65. claude_mpm/commander/llm/openrouter_client.py +167 -0
  66. claude_mpm/commander/llm/summarizer.py +70 -0
  67. claude_mpm/commander/memory/__init__.py +45 -0
  68. claude_mpm/commander/memory/compression.py +347 -0
  69. claude_mpm/commander/memory/embeddings.py +230 -0
  70. claude_mpm/commander/memory/entities.py +310 -0
  71. claude_mpm/commander/memory/example_usage.py +290 -0
  72. claude_mpm/commander/memory/integration.py +325 -0
  73. claude_mpm/commander/memory/search.py +381 -0
  74. claude_mpm/commander/memory/store.py +657 -0
  75. claude_mpm/commander/models/__init__.py +18 -0
  76. claude_mpm/commander/models/events.py +121 -0
  77. claude_mpm/commander/models/project.py +162 -0
  78. claude_mpm/commander/models/work.py +214 -0
  79. claude_mpm/commander/parsing/__init__.py +20 -0
  80. claude_mpm/commander/parsing/extractor.py +132 -0
  81. claude_mpm/commander/parsing/output_parser.py +270 -0
  82. claude_mpm/commander/parsing/patterns.py +100 -0
  83. claude_mpm/commander/persistence/__init__.py +11 -0
  84. claude_mpm/commander/persistence/event_store.py +274 -0
  85. claude_mpm/commander/persistence/state_store.py +309 -0
  86. claude_mpm/commander/persistence/work_store.py +164 -0
  87. claude_mpm/commander/polling/__init__.py +13 -0
  88. claude_mpm/commander/polling/event_detector.py +104 -0
  89. claude_mpm/commander/polling/output_buffer.py +49 -0
  90. claude_mpm/commander/polling/output_poller.py +153 -0
  91. claude_mpm/commander/project_session.py +268 -0
  92. claude_mpm/commander/proxy/__init__.py +12 -0
  93. claude_mpm/commander/proxy/formatter.py +89 -0
  94. claude_mpm/commander/proxy/output_handler.py +191 -0
  95. claude_mpm/commander/proxy/relay.py +155 -0
  96. claude_mpm/commander/registry.py +410 -0
  97. claude_mpm/commander/runtime/__init__.py +10 -0
  98. claude_mpm/commander/runtime/executor.py +191 -0
  99. claude_mpm/commander/runtime/monitor.py +346 -0
  100. claude_mpm/commander/session/__init__.py +6 -0
  101. claude_mpm/commander/session/context.py +81 -0
  102. claude_mpm/commander/session/manager.py +59 -0
  103. claude_mpm/commander/tmux_orchestrator.py +361 -0
  104. claude_mpm/commander/web/__init__.py +1 -0
  105. claude_mpm/commander/work/__init__.py +30 -0
  106. claude_mpm/commander/work/executor.py +207 -0
  107. claude_mpm/commander/work/queue.py +405 -0
  108. claude_mpm/commander/workflow/__init__.py +27 -0
  109. claude_mpm/commander/workflow/event_handler.py +241 -0
  110. claude_mpm/commander/workflow/notifier.py +146 -0
  111. claude_mpm/commands/mpm-config.md +8 -0
  112. claude_mpm/commands/mpm-doctor.md +8 -0
  113. claude_mpm/commands/mpm-help.md +8 -0
  114. claude_mpm/commands/mpm-init.md +8 -0
  115. claude_mpm/commands/mpm-monitor.md +8 -0
  116. claude_mpm/commands/mpm-organize.md +8 -0
  117. claude_mpm/commands/mpm-postmortem.md +8 -0
  118. claude_mpm/commands/mpm-session-resume.md +8 -0
  119. claude_mpm/commands/mpm-status.md +8 -0
  120. claude_mpm/commands/mpm-ticket-view.md +8 -0
  121. claude_mpm/commands/mpm-version.md +8 -0
  122. claude_mpm/commands/mpm.md +8 -0
  123. claude_mpm/config/agent_presets.py +8 -7
  124. claude_mpm/config/skill_sources.py +16 -0
  125. claude_mpm/core/claude_runner.py +143 -0
  126. claude_mpm/core/config.py +32 -19
  127. claude_mpm/core/logger.py +26 -9
  128. claude_mpm/core/logging_utils.py +35 -11
  129. claude_mpm/core/output_style_manager.py +49 -12
  130. claude_mpm/core/unified_config.py +10 -6
  131. claude_mpm/core/unified_paths.py +68 -80
  132. claude_mpm/experimental/cli_enhancements.py +2 -1
  133. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-312.pyc +0 -0
  134. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-314.pyc +0 -0
  135. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  136. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-312.pyc +0 -0
  137. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-314.pyc +0 -0
  138. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  139. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-312.pyc +0 -0
  140. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-314.pyc +0 -0
  141. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  142. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-312.pyc +0 -0
  143. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-314.pyc +0 -0
  144. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  145. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-314.pyc +0 -0
  146. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  147. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-312.pyc +0 -0
  148. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-314.pyc +0 -0
  149. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  150. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-312.pyc +0 -0
  151. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-314.pyc +0 -0
  152. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-312.pyc +0 -0
  153. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-314.pyc +0 -0
  154. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +29 -30
  155. claude_mpm/hooks/claude_hooks/event_handlers.py +112 -99
  156. claude_mpm/hooks/claude_hooks/hook_handler.py +81 -88
  157. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +6 -11
  158. claude_mpm/hooks/claude_hooks/installer.py +116 -8
  159. claude_mpm/hooks/claude_hooks/memory_integration.py +51 -31
  160. claude_mpm/hooks/claude_hooks/response_tracking.py +39 -58
  161. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-312.pyc +0 -0
  162. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-314.pyc +0 -0
  163. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  164. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  165. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-312.pyc +0 -0
  166. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-314.pyc +0 -0
  167. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-312.pyc +0 -0
  168. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-314.pyc +0 -0
  169. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  170. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-312.pyc +0 -0
  171. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-314.pyc +0 -0
  172. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  173. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-312.pyc +0 -0
  174. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-314.pyc +0 -0
  175. claude_mpm/hooks/claude_hooks/services/connection_manager.py +23 -28
  176. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +36 -103
  177. claude_mpm/hooks/claude_hooks/services/state_manager.py +23 -36
  178. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +47 -73
  179. claude_mpm/hooks/session_resume_hook.py +22 -18
  180. claude_mpm/hooks/templates/pre_tool_use_template.py +10 -2
  181. claude_mpm/scripts/claude-hook-handler.sh +43 -16
  182. claude_mpm/scripts/start_activity_logging.py +0 -0
  183. claude_mpm/services/agents/agent_recommendation_service.py +8 -8
  184. claude_mpm/services/agents/agent_selection_service.py +2 -2
  185. claude_mpm/services/agents/loading/framework_agent_loader.py +75 -2
  186. claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
  187. claude_mpm/services/event_log.py +8 -0
  188. claude_mpm/services/pm_skills_deployer.py +84 -6
  189. claude_mpm/services/skills/git_skill_source_manager.py +130 -10
  190. claude_mpm/services/skills/selective_skill_deployer.py +28 -0
  191. claude_mpm/services/skills/skill_discovery_service.py +74 -4
  192. claude_mpm/services/skills_deployer.py +31 -5
  193. claude_mpm/skills/__init__.py +2 -1
  194. claude_mpm/skills/bundled/pm/mpm/SKILL.md +38 -0
  195. claude_mpm/skills/bundled/pm/mpm-config/SKILL.md +29 -0
  196. claude_mpm/skills/bundled/pm/mpm-doctor/SKILL.md +53 -0
  197. claude_mpm/skills/bundled/pm/mpm-help/SKILL.md +35 -0
  198. claude_mpm/skills/bundled/pm/mpm-init/SKILL.md +125 -0
  199. claude_mpm/skills/bundled/pm/mpm-monitor/SKILL.md +32 -0
  200. claude_mpm/skills/bundled/pm/mpm-organize/SKILL.md +121 -0
  201. claude_mpm/skills/bundled/pm/mpm-postmortem/SKILL.md +22 -0
  202. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  203. claude_mpm/skills/bundled/pm/mpm-session-resume/SKILL.md +31 -0
  204. claude_mpm/skills/bundled/pm/mpm-status/SKILL.md +37 -0
  205. claude_mpm/skills/bundled/pm/mpm-ticket-view/SKILL.md +110 -0
  206. claude_mpm/skills/bundled/pm/mpm-version/SKILL.md +21 -0
  207. claude_mpm/skills/registry.py +295 -90
  208. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/METADATA +22 -6
  209. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/RECORD +213 -83
  210. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/WHEEL +0 -0
  211. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/entry_points.txt +0 -0
  212. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/licenses/LICENSE +0 -0
  213. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  214. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,325 @@
1
+ """Integration helpers for memory system with Commander.
2
+
3
+ Provides high-level functions to integrate conversation memory with
4
+ RuntimeMonitor, Chat CLI, and session resumption workflows.
5
+ """
6
+
7
+ import logging
8
+ import uuid
9
+ from datetime import datetime, timezone
10
+ from typing import List, Optional
11
+
12
+ from ..llm.openrouter_client import OpenRouterClient
13
+ from ..models.project import Project, ThreadMessage
14
+ from .compression import ContextCompressor
15
+ from .embeddings import EmbeddingService
16
+ from .entities import EntityExtractor
17
+ from .search import SemanticSearch
18
+ from .store import Conversation, ConversationMessage, ConversationStore
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class MemoryIntegration:
24
+ """High-level memory integration for Commander.
25
+
26
+ Provides simple API for common memory operations:
27
+ - Capture conversation from Project
28
+ - Search across all conversations
29
+ - Load context for session resume
30
+
31
+ Attributes:
32
+ store: ConversationStore for persistence
33
+ embeddings: EmbeddingService for vectors
34
+ search: SemanticSearch for queries
35
+ compressor: ContextCompressor for summaries
36
+ extractor: EntityExtractor for entity extraction
37
+
38
+ Example:
39
+ >>> memory = MemoryIntegration.create()
40
+ >>> await memory.capture_project_conversation(project)
41
+ >>> results = await memory.search("login bug fix", project_id="proj-xyz")
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ store: ConversationStore,
47
+ embeddings: EmbeddingService,
48
+ search: SemanticSearch,
49
+ compressor: ContextCompressor,
50
+ extractor: EntityExtractor,
51
+ ):
52
+ """Initialize memory integration.
53
+
54
+ Args:
55
+ store: ConversationStore instance
56
+ embeddings: EmbeddingService instance
57
+ search: SemanticSearch instance
58
+ compressor: ContextCompressor instance
59
+ extractor: EntityExtractor instance
60
+ """
61
+ self.store = store
62
+ self.embeddings = embeddings
63
+ self.search = search
64
+ self.compressor = compressor
65
+ self.extractor = extractor
66
+
67
+ logger.info("MemoryIntegration initialized")
68
+
69
+ @classmethod
70
+ def create(
71
+ cls,
72
+ openrouter_client: Optional[OpenRouterClient] = None,
73
+ embedding_provider: str = "sentence-transformers",
74
+ ) -> "MemoryIntegration":
75
+ """Create MemoryIntegration with default configuration.
76
+
77
+ Args:
78
+ openrouter_client: Optional OpenRouterClient for summarization
79
+ embedding_provider: Embedding provider ("sentence-transformers" or "openai")
80
+
81
+ Returns:
82
+ Configured MemoryIntegration instance
83
+
84
+ Example:
85
+ >>> from claude_mpm.commander.llm import OpenRouterClient
86
+ >>> client = OpenRouterClient()
87
+ >>> memory = MemoryIntegration.create(openrouter_client=client)
88
+ """
89
+ store = ConversationStore()
90
+ embeddings = EmbeddingService(provider=embedding_provider)
91
+ search = SemanticSearch(store, embeddings)
92
+
93
+ # Create OpenRouter client if not provided
94
+ if openrouter_client is None:
95
+ openrouter_client = OpenRouterClient()
96
+
97
+ compressor = ContextCompressor(openrouter_client)
98
+ extractor = EntityExtractor()
99
+
100
+ return cls(store, embeddings, search, compressor, extractor)
101
+
102
+ async def capture_project_conversation(
103
+ self,
104
+ project: Project,
105
+ instance_name: str = "unknown",
106
+ session_id: Optional[str] = None,
107
+ ) -> Conversation:
108
+ """Capture conversation from Project thread.
109
+
110
+ Converts Project.thread (List[ThreadMessage]) into a Conversation
111
+ with entity extraction and optional summarization.
112
+
113
+ Args:
114
+ project: Project with conversation thread
115
+ instance_name: Instance name (e.g., "claude-code-1")
116
+ session_id: Optional session ID
117
+
118
+ Returns:
119
+ Captured and saved Conversation
120
+
121
+ Example:
122
+ >>> conv = await memory.capture_project_conversation(project)
123
+ >>> print(f"Captured conversation {conv.id} with {len(conv.messages)} messages")
124
+ """
125
+ if not project.thread:
126
+ logger.warning("Project %s has no conversation thread", project.id)
127
+ return None
128
+
129
+ # Convert ThreadMessages to ConversationMessages
130
+ messages = []
131
+ for thread_msg in project.thread:
132
+ conv_msg = ConversationMessage.from_thread_message(thread_msg)
133
+
134
+ # Extract entities
135
+ entities = self.extractor.extract(conv_msg.content)
136
+ conv_msg.entities = [e.to_dict() for e in entities]
137
+
138
+ messages.append(conv_msg)
139
+
140
+ # Create conversation
141
+ conversation = Conversation(
142
+ id=f"conv-{uuid.uuid4().hex[:12]}",
143
+ project_id=project.id,
144
+ instance_name=instance_name,
145
+ session_id=session_id or f"sess-{uuid.uuid4().hex[:8]}",
146
+ messages=messages,
147
+ )
148
+
149
+ # Auto-summarize if needed
150
+ if self.compressor.needs_summarization(messages):
151
+ conversation.summary = await self.compressor.summarize(messages)
152
+ logger.info("Auto-generated summary for conversation %s", conversation.id)
153
+
154
+ # Generate embedding for semantic search
155
+ text_for_embedding = conversation.summary or conversation.get_full_text()[:1000]
156
+ conversation.embedding = await self.embeddings.embed(text_for_embedding)
157
+
158
+ # Save to store
159
+ await self.store.save(conversation)
160
+
161
+ logger.info(
162
+ "Captured conversation %s (%d messages) from project %s",
163
+ conversation.id,
164
+ len(messages),
165
+ project.id,
166
+ )
167
+
168
+ return conversation
169
+
170
+ async def search_conversations(
171
+ self,
172
+ query: str,
173
+ project_id: Optional[str] = None,
174
+ limit: int = 10,
175
+ ) -> List:
176
+ """Search conversations by natural language query.
177
+
178
+ Args:
179
+ query: Natural language search query
180
+ project_id: Optional project filter
181
+ limit: Maximum results
182
+
183
+ Returns:
184
+ List of SearchResult with conversations
185
+
186
+ Example:
187
+ >>> results = await memory.search_conversations(
188
+ ... "how did we fix the authentication bug?",
189
+ ... project_id="proj-xyz",
190
+ ... limit=5
191
+ ... )
192
+ >>> for result in results:
193
+ ... print(f"{result.score:.2f}: {result.snippet}")
194
+ """
195
+ return await self.search.search(query, project_id=project_id, limit=limit)
196
+
197
+ async def load_context_for_session(
198
+ self,
199
+ project_id: str,
200
+ max_tokens: int = 4000,
201
+ limit_conversations: int = 10,
202
+ ) -> str:
203
+ """Load compressed context for session resumption.
204
+
205
+ Retrieves recent conversations from project and compresses them
206
+ into a context string suitable for LLM input.
207
+
208
+ Args:
209
+ project_id: Project ID to load context for
210
+ max_tokens: Maximum tokens for context
211
+ limit_conversations: Maximum conversations to consider
212
+
213
+ Returns:
214
+ Compressed context string
215
+
216
+ Example:
217
+ >>> context = await memory.load_context_for_session("proj-xyz")
218
+ >>> print(f"Loaded context: {len(context)} chars")
219
+ """
220
+ # Get recent conversations from project
221
+ conversations = await self.store.list_by_project(
222
+ project_id, limit=limit_conversations
223
+ )
224
+
225
+ if not conversations:
226
+ logger.info("No conversations found for project %s", project_id)
227
+ return ""
228
+
229
+ # Compress into context
230
+ context = await self.compressor.compress_for_context(
231
+ conversations, max_tokens=max_tokens
232
+ )
233
+
234
+ logger.info(
235
+ "Loaded context for project %s: %d conversations, %d chars",
236
+ project_id,
237
+ len(conversations),
238
+ len(context),
239
+ )
240
+
241
+ return context
242
+
243
+ async def update_conversation(
244
+ self,
245
+ conversation_id: str,
246
+ new_messages: List[ThreadMessage],
247
+ ) -> Optional[Conversation]:
248
+ """Update existing conversation with new messages.
249
+
250
+ Args:
251
+ conversation_id: Conversation to update
252
+ new_messages: New messages to append
253
+
254
+ Returns:
255
+ Updated conversation if found, None otherwise
256
+
257
+ Example:
258
+ >>> updated = await memory.update_conversation(
259
+ ... "conv-abc123",
260
+ ... [new_message1, new_message2]
261
+ ... )
262
+ """
263
+ # Load existing conversation
264
+ conversation = await self.store.load(conversation_id)
265
+ if not conversation:
266
+ logger.warning("Conversation %s not found", conversation_id)
267
+ return None
268
+
269
+ # Convert and append new messages
270
+ for thread_msg in new_messages:
271
+ conv_msg = ConversationMessage.from_thread_message(thread_msg)
272
+
273
+ # Extract entities
274
+ entities = self.extractor.extract(conv_msg.content)
275
+ conv_msg.entities = [e.to_dict() for e in entities]
276
+
277
+ conversation.messages.append(conv_msg)
278
+
279
+ # Update timestamp
280
+ conversation.updated_at = datetime.now(timezone.utc)
281
+
282
+ # Regenerate summary if needed
283
+ updated_summary = await self.compressor.update_summary_if_stale(
284
+ conversation, message_threshold=5
285
+ )
286
+ if updated_summary:
287
+ conversation.summary = updated_summary
288
+
289
+ # Regenerate embedding
290
+ text_for_embedding = conversation.summary or conversation.get_full_text()[:1000]
291
+ conversation.embedding = await self.embeddings.embed(text_for_embedding)
292
+
293
+ # Save
294
+ await self.store.save(conversation)
295
+
296
+ logger.info(
297
+ "Updated conversation %s (now %d messages)",
298
+ conversation_id,
299
+ len(conversation.messages),
300
+ )
301
+
302
+ return conversation
303
+
304
+ async def get_conversation_by_session(
305
+ self, session_id: str
306
+ ) -> Optional[Conversation]:
307
+ """Get conversation by session ID.
308
+
309
+ Args:
310
+ session_id: Session ID from ToolSession
311
+
312
+ Returns:
313
+ Conversation if found, None otherwise
314
+
315
+ Example:
316
+ >>> conv = await memory.get_conversation_by_session("sess-abc123")
317
+ """
318
+ # For now, this requires loading and checking
319
+ # In production, you'd add an index on session_id
320
+ # This is a placeholder - implement proper query in store
321
+ logger.warning(
322
+ "get_conversation_by_session requires optimization - "
323
+ "add session_id index to store"
324
+ )
325
+ return None
@@ -0,0 +1,381 @@
1
+ """Semantic search for conversations.
2
+
3
+ Provides vector-based and text-based search across all stored conversations
4
+ with filtering by project, date range, and entity types.
5
+ """
6
+
7
+ import logging
8
+ from dataclasses import dataclass
9
+ from datetime import datetime
10
+ from typing import List, Optional, Tuple
11
+
12
+ from .embeddings import EmbeddingService
13
+ from .entities import EntityType
14
+ from .store import Conversation, ConversationStore
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @dataclass
20
+ class SearchResult:
21
+ """Search result with relevance score.
22
+
23
+ Attributes:
24
+ conversation: Matched conversation
25
+ score: Relevance score (0-1, higher is more relevant)
26
+ matched_entities: Entities that matched the query
27
+ snippet: Relevant text snippet
28
+
29
+ Example:
30
+ >>> result = search_results[0]
31
+ >>> print(f"Score: {result.score:.3f}")
32
+ >>> print(f"Conversation: {result.conversation.id}")
33
+ """
34
+
35
+ conversation: Conversation
36
+ score: float
37
+ matched_entities: List[str] = None
38
+ snippet: str = ""
39
+
40
+ def __post_init__(self) -> None:
41
+ """Initialize matched_entities if not provided."""
42
+ if self.matched_entities is None:
43
+ self.matched_entities = []
44
+
45
+
46
+ class SemanticSearch:
47
+ """Semantic search across conversations.
48
+
49
+ Combines vector similarity search with text search and entity filtering
50
+ for comprehensive conversation retrieval.
51
+
52
+ Attributes:
53
+ store: ConversationStore for persistence
54
+ embeddings: EmbeddingService for vector generation
55
+
56
+ Example:
57
+ >>> search = SemanticSearch(store, embeddings)
58
+ >>> results = await search.search(
59
+ ... "how did we fix the login bug?",
60
+ ... project_id="proj-xyz",
61
+ ... limit=5
62
+ ... )
63
+ """
64
+
65
+ def __init__(self, store: ConversationStore, embeddings: EmbeddingService):
66
+ """Initialize semantic search.
67
+
68
+ Args:
69
+ store: ConversationStore for conversation persistence
70
+ embeddings: EmbeddingService for vector generation
71
+ """
72
+ self.store = store
73
+ self.embeddings = embeddings
74
+
75
+ logger.info("SemanticSearch initialized (vector: %s)", store.enable_vector)
76
+
77
+ async def search(
78
+ self,
79
+ query: str,
80
+ project_id: Optional[str] = None,
81
+ limit: int = 10,
82
+ date_range: Optional[Tuple[datetime, datetime]] = None,
83
+ entity_types: Optional[List[EntityType]] = None,
84
+ ) -> List[SearchResult]:
85
+ """Search conversations semantically.
86
+
87
+ Uses vector similarity if available, falls back to text search.
88
+ Results are ranked by relevance score.
89
+
90
+ Args:
91
+ query: Natural language search query
92
+ project_id: Optional project ID filter
93
+ limit: Maximum number of results
94
+ date_range: Optional (start, end) datetime filter
95
+ entity_types: Optional list of entity types to filter by
96
+
97
+ Returns:
98
+ List of SearchResult ordered by relevance (highest first)
99
+
100
+ Example:
101
+ >>> results = await search.search(
102
+ ... "login bug fix",
103
+ ... project_id="proj-xyz",
104
+ ... limit=5
105
+ ... )
106
+ >>> print(f"Found {len(results)} results")
107
+ >>> print(f"Top result: {results[0].conversation.summary}")
108
+ """
109
+ logger.debug(
110
+ "Searching conversations (query: %s, project: %s, limit: %d)",
111
+ query[:50],
112
+ project_id or "all",
113
+ limit,
114
+ )
115
+
116
+ if self.store.enable_vector:
117
+ # Use vector search
118
+ results = await self._vector_search(query, project_id, limit)
119
+ else:
120
+ # Fall back to text search
121
+ conversations = await self.store.search_by_text(query, project_id, limit)
122
+ results = [
123
+ SearchResult(conversation=conv, score=0.5) for conv in conversations
124
+ ]
125
+
126
+ # Apply date range filter
127
+ if date_range:
128
+ start, end = date_range
129
+ results = [r for r in results if start <= r.conversation.updated_at <= end]
130
+
131
+ # Apply entity type filter
132
+ if entity_types:
133
+ results = self._filter_by_entities(results, entity_types)
134
+
135
+ # Generate snippets for top results
136
+ for result in results[:limit]:
137
+ result.snippet = self._generate_snippet(result.conversation, query)
138
+
139
+ logger.info("Search returned %d results", len(results))
140
+ return results[:limit]
141
+
142
+ async def _vector_search(
143
+ self,
144
+ query: str,
145
+ project_id: Optional[str] = None,
146
+ limit: int = 10,
147
+ ) -> List[SearchResult]:
148
+ """Perform vector similarity search.
149
+
150
+ Args:
151
+ query: Search query
152
+ project_id: Optional project filter
153
+ limit: Maximum results
154
+
155
+ Returns:
156
+ List of SearchResult with similarity scores
157
+ """
158
+ # Generate query embedding
159
+ query_embedding = await self.embeddings.embed(query)
160
+
161
+ # Get all conversations (TODO: optimize with vector DB query)
162
+ if project_id:
163
+ conversations = await self.store.list_by_project(project_id, limit=100)
164
+ else:
165
+ # For now, we'll search recent conversations across all projects
166
+ # In production, you'd want to implement proper vector search in SQL
167
+ conversations = []
168
+ logger.warning(
169
+ "Vector search without project_id not fully optimized. "
170
+ "Consider implementing KNN query in SQL."
171
+ )
172
+
173
+ # Calculate similarities
174
+ results = []
175
+ for conv in conversations:
176
+ if not conv.embedding:
177
+ # Generate embedding if missing
178
+ text = conv.summary or conv.get_full_text()[:1000]
179
+ conv.embedding = await self.embeddings.embed(text)
180
+ await self.store.save(conv)
181
+
182
+ # Calculate cosine similarity
183
+ score = self.embeddings.cosine_similarity(query_embedding, conv.embedding)
184
+ results.append(SearchResult(conversation=conv, score=score))
185
+
186
+ # Sort by score descending
187
+ results.sort(key=lambda r: r.score, reverse=True)
188
+
189
+ return results[:limit]
190
+
191
+ def _filter_by_entities(
192
+ self, results: List[SearchResult], entity_types: List[EntityType]
193
+ ) -> List[SearchResult]:
194
+ """Filter results by entity types.
195
+
196
+ Args:
197
+ results: Search results to filter
198
+ entity_types: Entity types to require
199
+
200
+ Returns:
201
+ Filtered results
202
+ """
203
+ filtered = []
204
+ for result in results:
205
+ # Check if conversation has any messages with matching entities
206
+ has_entity = False
207
+ for msg in result.conversation.messages:
208
+ for entity in msg.entities:
209
+ if EntityType(entity["type"]) in entity_types:
210
+ has_entity = True
211
+ if "matched_entities" not in result.matched_entities:
212
+ result.matched_entities.append(entity["value"])
213
+
214
+ if has_entity:
215
+ filtered.append(result)
216
+
217
+ return filtered
218
+
219
+ def _generate_snippet(self, conversation: Conversation, query: str) -> str:
220
+ """Generate relevant text snippet from conversation.
221
+
222
+ Args:
223
+ conversation: Conversation to extract snippet from
224
+ query: Search query for context
225
+
226
+ Returns:
227
+ Relevant snippet (max 200 chars)
228
+ """
229
+ # Try to find message containing query terms
230
+ query_terms = query.lower().split()
231
+
232
+ for msg in conversation.messages:
233
+ content_lower = msg.content.lower()
234
+ if any(term in content_lower for term in query_terms):
235
+ # Found relevant message, extract snippet
236
+ start_idx = 0
237
+ for term in query_terms:
238
+ if term in content_lower:
239
+ start_idx = content_lower.index(term)
240
+ break
241
+
242
+ # Extract context around match
243
+ snippet_start = max(0, start_idx - 50)
244
+ snippet_end = min(len(msg.content), start_idx + 150)
245
+ snippet = msg.content[snippet_start:snippet_end]
246
+
247
+ # Add ellipsis if truncated
248
+ if snippet_start > 0:
249
+ snippet = "..." + snippet
250
+ if snippet_end < len(msg.content):
251
+ snippet = snippet + "..."
252
+
253
+ return snippet.strip()
254
+
255
+ # No match found, use summary or first message
256
+ if conversation.summary:
257
+ return conversation.summary[:200]
258
+ if conversation.messages:
259
+ return conversation.messages[0].content[:200] + "..."
260
+
261
+ return ""
262
+
263
+ async def find_similar(
264
+ self,
265
+ conversation_id: str,
266
+ limit: int = 5,
267
+ ) -> List[SearchResult]:
268
+ """Find conversations similar to a given conversation.
269
+
270
+ Args:
271
+ conversation_id: Reference conversation ID
272
+ limit: Maximum number of similar conversations
273
+
274
+ Returns:
275
+ List of similar conversations ordered by similarity
276
+
277
+ Example:
278
+ >>> similar = await search.find_similar("conv-abc123", limit=5)
279
+ """
280
+ # Load reference conversation
281
+ ref_conv = await self.store.load(conversation_id)
282
+ if not ref_conv:
283
+ logger.warning("Conversation %s not found", conversation_id)
284
+ return []
285
+
286
+ # Ensure embedding exists
287
+ if not ref_conv.embedding:
288
+ text = ref_conv.summary or ref_conv.get_full_text()[:1000]
289
+ ref_conv.embedding = await self.embeddings.embed(text)
290
+ await self.store.save(ref_conv)
291
+
292
+ # Get all conversations from same project
293
+ all_convs = await self.store.list_by_project(ref_conv.project_id, limit=100)
294
+
295
+ # Calculate similarities
296
+ results = []
297
+ for conv in all_convs:
298
+ # Skip the reference conversation itself
299
+ if conv.id == conversation_id:
300
+ continue
301
+
302
+ if not conv.embedding:
303
+ text = conv.summary or conv.get_full_text()[:1000]
304
+ conv.embedding = await self.embeddings.embed(text)
305
+ await self.store.save(conv)
306
+
307
+ score = self.embeddings.cosine_similarity(
308
+ ref_conv.embedding, conv.embedding
309
+ )
310
+ results.append(SearchResult(conversation=conv, score=score))
311
+
312
+ # Sort by similarity
313
+ results.sort(key=lambda r: r.score, reverse=True)
314
+
315
+ logger.info(
316
+ "Found %d similar conversations to %s",
317
+ len(results[:limit]),
318
+ conversation_id,
319
+ )
320
+ return results[:limit]
321
+
322
+ async def search_by_entities(
323
+ self,
324
+ entity_type: EntityType,
325
+ entity_value: str,
326
+ project_id: Optional[str] = None,
327
+ limit: int = 10,
328
+ ) -> List[SearchResult]:
329
+ """Search conversations by specific entity.
330
+
331
+ Args:
332
+ entity_type: Type of entity to search for
333
+ entity_value: Entity value (e.g., "src/auth.py")
334
+ project_id: Optional project filter
335
+ limit: Maximum results
336
+
337
+ Returns:
338
+ List of conversations containing the entity
339
+
340
+ Example:
341
+ >>> results = await search.search_by_entities(
342
+ ... EntityType.FILE,
343
+ ... "src/auth.py",
344
+ ... project_id="proj-xyz"
345
+ ... )
346
+ """
347
+ # Get conversations from project
348
+ if project_id:
349
+ conversations = await self.store.list_by_project(project_id, limit=100)
350
+ else:
351
+ conversations = []
352
+ logger.warning("Entity search without project_id requires full scan")
353
+
354
+ # Filter by entity
355
+ results = []
356
+ for conv in conversations:
357
+ for msg in conv.messages:
358
+ for entity in msg.entities:
359
+ if (
360
+ entity.get("type") == entity_type.value
361
+ and entity.get("value") == entity_value
362
+ ):
363
+ results.append(
364
+ SearchResult(
365
+ conversation=conv,
366
+ score=1.0, # Exact match
367
+ matched_entities=[entity_value],
368
+ )
369
+ )
370
+ break
371
+
372
+ if len(results) >= limit:
373
+ break
374
+
375
+ logger.info(
376
+ "Found %d conversations with entity %s:%s",
377
+ len(results),
378
+ entity_type.value,
379
+ entity_value,
380
+ )
381
+ return results[:limit]