gobby 0.2.6__py3-none-any.whl → 0.2.7__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 (146) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/codex_impl/__init__.py +28 -0
  4. gobby/adapters/codex_impl/adapter.py +722 -0
  5. gobby/adapters/codex_impl/client.py +679 -0
  6. gobby/adapters/codex_impl/protocol.py +20 -0
  7. gobby/adapters/codex_impl/types.py +68 -0
  8. gobby/agents/definitions.py +11 -1
  9. gobby/agents/isolation.py +395 -0
  10. gobby/agents/sandbox.py +261 -0
  11. gobby/agents/spawn.py +42 -287
  12. gobby/agents/spawn_executor.py +385 -0
  13. gobby/agents/spawners/__init__.py +24 -0
  14. gobby/agents/spawners/command_builder.py +189 -0
  15. gobby/agents/spawners/embedded.py +21 -2
  16. gobby/agents/spawners/headless.py +21 -2
  17. gobby/agents/spawners/prompt_manager.py +125 -0
  18. gobby/cli/install.py +4 -4
  19. gobby/cli/installers/claude.py +6 -0
  20. gobby/cli/installers/gemini.py +6 -0
  21. gobby/cli/installers/shared.py +103 -4
  22. gobby/cli/sessions.py +1 -1
  23. gobby/cli/utils.py +9 -2
  24. gobby/config/__init__.py +12 -97
  25. gobby/config/app.py +10 -94
  26. gobby/config/extensions.py +2 -2
  27. gobby/config/features.py +7 -130
  28. gobby/config/tasks.py +4 -28
  29. gobby/hooks/__init__.py +0 -13
  30. gobby/hooks/event_handlers.py +45 -2
  31. gobby/hooks/hook_manager.py +2 -2
  32. gobby/hooks/plugins.py +1 -1
  33. gobby/hooks/webhooks.py +1 -1
  34. gobby/llm/resolver.py +3 -2
  35. gobby/mcp_proxy/importer.py +62 -4
  36. gobby/mcp_proxy/instructions.py +2 -0
  37. gobby/mcp_proxy/registries.py +1 -4
  38. gobby/mcp_proxy/services/recommendation.py +43 -11
  39. gobby/mcp_proxy/tools/agents.py +31 -731
  40. gobby/mcp_proxy/tools/clones.py +0 -385
  41. gobby/mcp_proxy/tools/memory.py +2 -2
  42. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  43. gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
  44. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  45. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  46. gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
  47. gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
  48. gobby/mcp_proxy/tools/skills/__init__.py +14 -29
  49. gobby/mcp_proxy/tools/spawn_agent.py +417 -0
  50. gobby/mcp_proxy/tools/tasks/_lifecycle.py +52 -18
  51. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
  52. gobby/mcp_proxy/tools/worktrees.py +0 -343
  53. gobby/memory/ingestion/__init__.py +5 -0
  54. gobby/memory/ingestion/multimodal.py +221 -0
  55. gobby/memory/manager.py +62 -283
  56. gobby/memory/search/__init__.py +10 -0
  57. gobby/memory/search/coordinator.py +248 -0
  58. gobby/memory/services/__init__.py +5 -0
  59. gobby/memory/services/crossref.py +142 -0
  60. gobby/prompts/loader.py +5 -2
  61. gobby/servers/http.py +1 -4
  62. gobby/servers/routes/admin.py +14 -0
  63. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  64. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  65. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  66. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  67. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  68. gobby/servers/routes/mcp/hooks.py +1 -1
  69. gobby/servers/routes/mcp/tools.py +48 -1506
  70. gobby/sessions/lifecycle.py +1 -1
  71. gobby/sessions/processor.py +10 -0
  72. gobby/sessions/transcripts/base.py +1 -0
  73. gobby/sessions/transcripts/claude.py +15 -5
  74. gobby/skills/parser.py +30 -2
  75. gobby/storage/migrations.py +159 -372
  76. gobby/storage/sessions.py +43 -7
  77. gobby/storage/skills.py +37 -4
  78. gobby/storage/tasks/_lifecycle.py +18 -3
  79. gobby/sync/memories.py +1 -1
  80. gobby/tasks/external_validator.py +1 -1
  81. gobby/tasks/validation.py +22 -20
  82. gobby/tools/summarizer.py +91 -10
  83. gobby/utils/project_context.py +2 -3
  84. gobby/utils/status.py +13 -0
  85. gobby/workflows/actions.py +221 -1217
  86. gobby/workflows/artifact_actions.py +31 -0
  87. gobby/workflows/autonomous_actions.py +11 -0
  88. gobby/workflows/context_actions.py +50 -1
  89. gobby/workflows/enforcement/__init__.py +47 -0
  90. gobby/workflows/enforcement/blocking.py +269 -0
  91. gobby/workflows/enforcement/commit_policy.py +283 -0
  92. gobby/workflows/enforcement/handlers.py +269 -0
  93. gobby/workflows/enforcement/task_policy.py +542 -0
  94. gobby/workflows/git_utils.py +106 -0
  95. gobby/workflows/llm_actions.py +30 -0
  96. gobby/workflows/mcp_actions.py +20 -1
  97. gobby/workflows/memory_actions.py +80 -0
  98. gobby/workflows/safe_evaluator.py +183 -0
  99. gobby/workflows/session_actions.py +44 -0
  100. gobby/workflows/state_actions.py +60 -1
  101. gobby/workflows/stop_signal_actions.py +55 -0
  102. gobby/workflows/summary_actions.py +94 -1
  103. gobby/workflows/task_sync_actions.py +347 -0
  104. gobby/workflows/todo_actions.py +34 -1
  105. gobby/workflows/webhook_actions.py +185 -0
  106. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/METADATA +6 -1
  107. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/RECORD +111 -111
  108. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
  109. gobby/adapters/codex.py +0 -1332
  110. gobby/install/claude/commands/gobby/bug.md +0 -51
  111. gobby/install/claude/commands/gobby/chore.md +0 -51
  112. gobby/install/claude/commands/gobby/epic.md +0 -52
  113. gobby/install/claude/commands/gobby/eval.md +0 -235
  114. gobby/install/claude/commands/gobby/feat.md +0 -49
  115. gobby/install/claude/commands/gobby/nit.md +0 -52
  116. gobby/install/claude/commands/gobby/ref.md +0 -52
  117. gobby/mcp_proxy/tools/session_messages.py +0 -1055
  118. gobby/prompts/defaults/expansion/system.md +0 -119
  119. gobby/prompts/defaults/expansion/user.md +0 -48
  120. gobby/prompts/defaults/external_validation/agent.md +0 -72
  121. gobby/prompts/defaults/external_validation/external.md +0 -63
  122. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  123. gobby/prompts/defaults/external_validation/system.md +0 -6
  124. gobby/prompts/defaults/features/import_mcp.md +0 -22
  125. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  126. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  127. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  128. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  129. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  130. gobby/prompts/defaults/features/server_description.md +0 -20
  131. gobby/prompts/defaults/features/server_description_system.md +0 -6
  132. gobby/prompts/defaults/features/task_description.md +0 -31
  133. gobby/prompts/defaults/features/task_description_system.md +0 -6
  134. gobby/prompts/defaults/features/tool_summary.md +0 -17
  135. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  136. gobby/prompts/defaults/handoff/compact.md +0 -63
  137. gobby/prompts/defaults/handoff/session_end.md +0 -57
  138. gobby/prompts/defaults/memory/extract.md +0 -61
  139. gobby/prompts/defaults/research/step.md +0 -58
  140. gobby/prompts/defaults/validation/criteria.md +0 -47
  141. gobby/prompts/defaults/validation/validate.md +0 -38
  142. gobby/storage/migrations_legacy.py +0 -1359
  143. gobby/workflows/task_enforcement_actions.py +0 -1343
  144. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
  145. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
  146. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
gobby/memory/manager.py CHANGED
@@ -1,21 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- import mimetypes
5
4
  from datetime import UTC, datetime
6
- from pathlib import Path
7
5
  from typing import TYPE_CHECKING, Any
8
6
 
9
- from gobby.config.app import MemoryConfig
7
+ from gobby.config.persistence import MemoryConfig
10
8
  from gobby.memory.backends import get_backend
11
9
  from gobby.memory.context import build_memory_context
12
- from gobby.memory.protocol import MediaAttachment, MemoryBackendProtocol
10
+ from gobby.memory.ingestion import MultimodalIngestor
11
+ from gobby.memory.protocol import MemoryBackendProtocol
12
+ from gobby.memory.search.coordinator import SearchCoordinator
13
+ from gobby.memory.services.crossref import CrossrefService
13
14
  from gobby.storage.database import DatabaseProtocol
14
15
  from gobby.storage.memories import LocalMemoryManager, Memory
15
16
 
16
17
  if TYPE_CHECKING:
17
18
  from gobby.llm.service import LLMService
18
- from gobby.memory.search import SearchBackend
19
19
 
20
20
  logger = logging.getLogger(__name__)
21
21
 
@@ -45,8 +45,25 @@ class MemoryManager:
45
45
  # The SQLiteBackend uses LocalMemoryManager internally
46
46
  self.storage = LocalMemoryManager(db)
47
47
 
48
- self._search_backend: SearchBackend | None = None
49
- self._search_backend_fitted = False
48
+ # Initialize extracted components
49
+ self._search_coordinator = SearchCoordinator(
50
+ storage=self.storage,
51
+ config=config,
52
+ db=db,
53
+ )
54
+
55
+ self._crossref_service = CrossrefService(
56
+ storage=self.storage,
57
+ config=config,
58
+ search_backend_getter=lambda: self._search_coordinator.search_backend,
59
+ ensure_fitted=self._search_coordinator.ensure_fitted,
60
+ )
61
+
62
+ self._multimodal_ingestor = MultimodalIngestor(
63
+ storage=self.storage,
64
+ backend=self._backend,
65
+ llm_service=llm_service,
66
+ )
50
67
 
51
68
  @property
52
69
  def llm_service(self) -> LLMService | None:
@@ -57,9 +74,11 @@ class MemoryManager:
57
74
  def llm_service(self, service: LLMService | None) -> None:
58
75
  """Set the LLM service for image description."""
59
76
  self._llm_service = service
77
+ # Keep multimodal ingestor in sync
78
+ self._multimodal_ingestor.llm_service = service
60
79
 
61
80
  @property
62
- def search_backend(self) -> SearchBackend:
81
+ def search_backend(self) -> Any:
63
82
  """
64
83
  Lazy-init search backend based on configuration.
65
84
 
@@ -67,50 +86,15 @@ class MemoryManager:
67
86
  - "tfidf" (default): Zero-dependency TF-IDF search
68
87
  - "text": Simple text substring matching
69
88
  """
70
- if self._search_backend is None:
71
- from gobby.memory.search import get_search_backend
72
-
73
- backend_type = getattr(self.config, "search_backend", "tfidf")
74
- logger.debug(f"Initializing search backend: {backend_type}")
75
-
76
- try:
77
- self._search_backend = get_search_backend(
78
- backend_type=backend_type,
79
- db=self.db,
80
- )
81
- except Exception as e:
82
- logger.warning(
83
- f"Failed to initialize {backend_type} backend: {e}. Falling back to tfidf"
84
- )
85
- self._search_backend = get_search_backend("tfidf")
86
-
87
- return self._search_backend
89
+ return self._search_coordinator.search_backend
88
90
 
89
91
  def _ensure_search_backend_fitted(self) -> None:
90
92
  """Ensure the search backend is fitted with current memories."""
91
- if self._search_backend_fitted:
92
- return
93
-
94
- backend = self.search_backend
95
- if not backend.needs_refit():
96
- self._search_backend_fitted = True
97
- return
98
-
99
- # Fit the backend with all memories
100
- memories = self.storage.list_memories(limit=10000)
101
- memory_tuples = [(m.id, m.content) for m in memories]
102
-
103
- try:
104
- backend.fit(memory_tuples)
105
- self._search_backend_fitted = True
106
- logger.info(f"Search backend fitted with {len(memory_tuples)} memories")
107
- except Exception as e:
108
- logger.error(f"Failed to fit search backend: {e}")
109
- raise
93
+ self._search_coordinator.ensure_fitted()
110
94
 
111
95
  def mark_search_refit_needed(self) -> None:
112
96
  """Mark that the search backend needs to be refitted."""
113
- self._search_backend_fitted = False
97
+ self._search_coordinator.mark_refit_needed()
114
98
 
115
99
  def reindex_search(self) -> dict[str, Any]:
116
100
  """
@@ -125,36 +109,7 @@ class MemoryManager:
125
109
  Returns:
126
110
  Dict with index statistics including memory_count, backend_type, etc.
127
111
  """
128
- # Get all memories
129
- memories = self.storage.list_memories(limit=10000)
130
- memory_tuples = [(m.id, m.content) for m in memories]
131
-
132
- # Force refit the backend
133
- backend = self.search_backend
134
- backend_type = getattr(self.config, "search_backend", "tfidf")
135
-
136
- try:
137
- backend.fit(memory_tuples)
138
- self._search_backend_fitted = True
139
-
140
- # Get backend stats
141
- stats = backend.get_stats() if hasattr(backend, "get_stats") else {}
142
-
143
- return {
144
- "success": True,
145
- "memory_count": len(memory_tuples),
146
- "backend_type": backend_type,
147
- "fitted": True,
148
- **stats,
149
- }
150
- except Exception as e:
151
- logger.error(f"Failed to reindex search backend: {e}")
152
- return {
153
- "success": False,
154
- "error": str(e),
155
- "memory_count": len(memory_tuples),
156
- "backend_type": backend_type,
157
- }
112
+ return self._search_coordinator.reindex()
158
113
 
159
114
  async def remember(
160
115
  self,
@@ -206,7 +161,7 @@ class MemoryManager:
206
161
  # Auto cross-reference if enabled
207
162
  if getattr(self.config, "auto_crossref", False):
208
163
  try:
209
- self._create_crossrefs(memory)
164
+ await self._crossref_service.create_crossrefs(memory)
210
165
  except Exception as e:
211
166
  # Don't fail the remember if crossref fails
212
167
  logger.warning(f"Auto-crossref failed for {memory.id}: {e}")
@@ -247,73 +202,19 @@ class MemoryManager:
247
202
  Raises:
248
203
  ValueError: If LLM service is not configured or image not found
249
204
  """
250
- path = Path(image_path)
251
- if not path.exists():
252
- raise ValueError(f"Image not found: {image_path}")
253
-
254
- # Get LLM provider for image description
255
- if not self._llm_service:
256
- raise ValueError(
257
- "LLM service not configured. Pass llm_service to MemoryManager "
258
- "to enable remember_with_image."
259
- )
260
-
261
- provider = self._llm_service.get_default_provider()
262
-
263
- # Generate image description
264
- description = await provider.describe_image(image_path, context=context)
265
-
266
- # Determine MIME type
267
- mime_type, _ = mimetypes.guess_type(str(path))
268
- if not mime_type:
269
- mime_type = "application/octet-stream"
270
-
271
- # Create media attachment
272
- media = MediaAttachment(
273
- media_type="image",
274
- content_path=str(path.absolute()),
275
- mime_type=mime_type,
276
- description=description,
277
- description_model=provider.provider_name,
278
- )
279
-
280
- # Store memory with media attachment via backend
281
- record = await self._backend.create(
282
- content=description,
205
+ memory = await self._multimodal_ingestor.remember_with_image(
206
+ image_path=image_path,
207
+ context=context,
283
208
  memory_type=memory_type,
284
209
  importance=importance,
285
210
  project_id=project_id,
286
211
  source_type=source_type,
287
212
  source_session_id=source_session_id,
288
213
  tags=tags,
289
- media=[media],
290
214
  )
291
-
292
215
  # Mark search index for refit
293
216
  self.mark_search_refit_needed()
294
-
295
- # Return as Memory object for backward compatibility
296
- # Note: The backend returns MemoryRecord, but we need Memory
297
- memory = self.storage.get_memory(record.id)
298
- if memory is not None:
299
- return memory
300
-
301
- # Fallback: construct Memory from MemoryRecord if storage lookup fails
302
- # This can happen with synthetic records from failed backend calls
303
- return Memory(
304
- id=record.id,
305
- content=record.content,
306
- memory_type=record.memory_type,
307
- created_at=record.created_at.isoformat(),
308
- updated_at=record.updated_at.isoformat()
309
- if record.updated_at
310
- else record.created_at.isoformat(),
311
- project_id=record.project_id,
312
- source_type=record.source_type,
313
- source_session_id=record.source_session_id,
314
- importance=record.importance,
315
- tags=record.tags,
316
- )
217
+ return memory
317
218
 
318
219
  async def remember_screenshot(
319
220
  self,
@@ -348,31 +249,8 @@ class MemoryManager:
348
249
  Raises:
349
250
  ValueError: If LLM service is not configured or screenshot bytes are empty
350
251
  """
351
- if not screenshot_bytes:
352
- raise ValueError("Screenshot bytes cannot be empty")
353
-
354
- # Determine resources directory using centralized utility
355
- from datetime import datetime as dt
356
-
357
- from gobby.cli.utils import get_resources_dir
358
- from gobby.utils.project_context import get_project_context
359
-
360
- ctx = get_project_context()
361
- project_path = ctx.get("path") if ctx else None
362
- resources_dir = get_resources_dir(project_path)
363
-
364
- # Generate timestamp-based filename
365
- timestamp = dt.now().strftime("%Y%m%d_%H%M%S_%f")
366
- filename = f"screenshot_{timestamp}.png"
367
- filepath = resources_dir / filename
368
-
369
- # Write screenshot to file
370
- filepath.write_bytes(screenshot_bytes)
371
- logger.debug(f"Saved screenshot to {filepath}")
372
-
373
- # Delegate to remember_with_image
374
- return await self.remember_with_image(
375
- image_path=str(filepath),
252
+ memory = await self._multimodal_ingestor.remember_screenshot(
253
+ screenshot_bytes=screenshot_bytes,
376
254
  context=context,
377
255
  memory_type=memory_type,
378
256
  importance=importance,
@@ -381,8 +259,11 @@ class MemoryManager:
381
259
  source_session_id=source_session_id,
382
260
  tags=tags,
383
261
  )
262
+ # Mark search index for refit
263
+ self.mark_search_refit_needed()
264
+ return memory
384
265
 
385
- def _create_crossrefs(
266
+ async def _create_crossrefs(
386
267
  self,
387
268
  memory: Memory,
388
269
  threshold: float | None = None,
@@ -402,46 +283,13 @@ class MemoryManager:
402
283
  Returns:
403
284
  Number of cross-references created
404
285
  """
405
- # Get thresholds from config or use defaults
406
- if threshold is None:
407
- threshold = getattr(self.config, "crossref_threshold", None)
408
- if threshold is None:
409
- threshold = 0.3
410
- if max_links is None:
411
- max_links = getattr(self.config, "crossref_max_links", None)
412
- if max_links is None:
413
- max_links = 5
414
-
415
- # Ensure search backend is fitted
416
- self._ensure_search_backend_fitted()
417
-
418
- # Search for similar memories
419
- similar = self.search_backend.search(memory.content, top_k=max_links + 1)
420
-
421
- # Create cross-references
422
- created = 0
423
- for other_id, score in similar:
424
- # Skip self-reference
425
- if other_id == memory.id:
426
- continue
427
-
428
- # Skip below threshold
429
- if score < threshold:
430
- continue
431
-
432
- # Create the crossref
433
- self.storage.create_crossref(memory.id, other_id, score)
434
- created += 1
435
-
436
- if created >= max_links:
437
- break
438
-
439
- if created > 0:
440
- logger.debug(f"Created {created} crossrefs for memory {memory.id}")
441
-
442
- return created
286
+ return await self._crossref_service.create_crossrefs(
287
+ memory=memory,
288
+ threshold=threshold,
289
+ max_links=max_links,
290
+ )
443
291
 
444
- def get_related(
292
+ async def get_related(
445
293
  self,
446
294
  memory_id: str,
447
295
  limit: int = 5,
@@ -458,21 +306,12 @@ class MemoryManager:
458
306
  Returns:
459
307
  List of related Memory objects, sorted by similarity
460
308
  """
461
- crossrefs = self.storage.get_crossrefs(
462
- memory_id, limit=limit, min_similarity=min_similarity
309
+ return await self._crossref_service.get_related(
310
+ memory_id=memory_id,
311
+ limit=limit,
312
+ min_similarity=min_similarity,
463
313
  )
464
314
 
465
- # Get the actual Memory objects
466
- memories = []
467
- for ref in crossrefs:
468
- # Get the "other" memory in the relationship
469
- other_id = ref.target_id if ref.source_id == memory_id else ref.source_id
470
- memory = self.get_memory(other_id)
471
- if memory:
472
- memories.append(memory)
473
-
474
- return memories
475
-
476
315
  def recall(
477
316
  self,
478
317
  query: str | None = None,
@@ -555,80 +394,20 @@ class MemoryManager:
555
394
  Uses the new search backend by default (TF-IDF),
556
395
  falling back to legacy semantic search if configured.
557
396
  """
558
- # Determine search mode from config or parameters
559
- if search_mode is None:
560
- search_mode = getattr(self.config, "search_backend", "tfidf")
561
-
562
397
  # Legacy compatibility: use_semantic is deprecated
563
398
  if use_semantic is not None:
564
399
  logger.warning("use_semantic argument is deprecated and ignored")
565
400
 
566
- # Use the search backend
567
- try:
568
- self._ensure_search_backend_fitted()
569
- # Fetch more results to allow for filtering
570
- fetch_multiplier = 3 if (tags_all or tags_any or tags_none) else 2
571
- results = self.search_backend.search(query, top_k=limit * fetch_multiplier)
572
-
573
- # Get the actual Memory objects
574
- memory_ids = [mid for mid, _ in results]
575
- memories = []
576
- for mid in memory_ids:
577
- memory = self.get_memory(mid)
578
- if memory:
579
- # Apply filters
580
- if project_id and memory.project_id != project_id:
581
- if memory.project_id is not None: # Allow global memories
582
- continue
583
- if min_importance and memory.importance < min_importance:
584
- continue
585
- # Apply tag filters
586
- if not self._passes_tag_filter(memory, tags_all, tags_any, tags_none):
587
- continue
588
- memories.append(memory)
589
- if len(memories) >= limit:
590
- break
591
-
592
- return memories
593
-
594
- except Exception as e:
595
- logger.warning(f"Search backend failed, falling back to text search: {e}")
596
- # Fall back to text search with tag filtering
597
- memories = self.storage.search_memories(
598
- query_text=query,
599
- project_id=project_id,
600
- limit=limit * 2,
601
- tags_all=tags_all,
602
- tags_any=tags_any,
603
- tags_none=tags_none,
604
- )
605
- if min_importance:
606
- memories = [m for m in memories if m.importance >= min_importance]
607
- return memories[:limit]
608
-
609
- def _passes_tag_filter(
610
- self,
611
- memory: Memory,
612
- tags_all: list[str] | None = None,
613
- tags_any: list[str] | None = None,
614
- tags_none: list[str] | None = None,
615
- ) -> bool:
616
- """Check if a memory passes the tag filter criteria."""
617
- memory_tags = set(memory.tags) if memory.tags else set()
618
-
619
- # Check tags_all: memory must have ALL specified tags
620
- if tags_all and not set(tags_all).issubset(memory_tags):
621
- return False
622
-
623
- # Check tags_any: memory must have at least ONE specified tag
624
- if tags_any and not memory_tags.intersection(tags_any):
625
- return False
626
-
627
- # Check tags_none: memory must have NONE of the specified tags
628
- if tags_none and memory_tags.intersection(tags_none):
629
- return False
630
-
631
- return True
401
+ return self._search_coordinator.search(
402
+ query=query,
403
+ project_id=project_id,
404
+ limit=limit,
405
+ min_importance=min_importance,
406
+ search_mode=search_mode,
407
+ tags_all=tags_all,
408
+ tags_any=tags_any,
409
+ tags_none=tags_none,
410
+ )
632
411
 
633
412
  def recall_as_context(
634
413
  self,
@@ -30,10 +30,20 @@ __all__ = [
30
30
  "SearchBackend",
31
31
  "SearchResult",
32
32
  "TFIDFSearcher",
33
+ "SearchCoordinator",
33
34
  "get_search_backend",
34
35
  ]
35
36
 
36
37
 
38
+ # Lazy import for SearchCoordinator to avoid circular imports
39
+ def __getattr__(name: str) -> Any:
40
+ if name == "SearchCoordinator":
41
+ from gobby.memory.search.coordinator import SearchCoordinator
42
+
43
+ return SearchCoordinator
44
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
45
+
46
+
37
47
  def get_search_backend(
38
48
  backend_type: str,
39
49
  db: DatabaseProtocol | None = None,