gobby 0.2.5__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.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +2 -1
- gobby/adapters/claude_code.py +13 -4
- gobby/adapters/codex_impl/__init__.py +28 -0
- gobby/adapters/codex_impl/adapter.py +722 -0
- gobby/adapters/codex_impl/client.py +679 -0
- gobby/adapters/codex_impl/protocol.py +20 -0
- gobby/adapters/codex_impl/types.py +68 -0
- gobby/agents/definitions.py +11 -1
- gobby/agents/isolation.py +395 -0
- gobby/agents/runner.py +8 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +385 -0
- gobby/agents/spawners/__init__.py +24 -0
- gobby/agents/spawners/command_builder.py +189 -0
- gobby/agents/spawners/embedded.py +21 -2
- gobby/agents/spawners/headless.py +21 -2
- gobby/agents/spawners/prompt_manager.py +125 -0
- gobby/cli/__init__.py +6 -0
- gobby/cli/clones.py +419 -0
- gobby/cli/conductor.py +266 -0
- gobby/cli/install.py +4 -4
- gobby/cli/installers/antigravity.py +3 -9
- gobby/cli/installers/claude.py +15 -9
- gobby/cli/installers/codex.py +2 -8
- gobby/cli/installers/gemini.py +8 -8
- gobby/cli/installers/shared.py +175 -13
- gobby/cli/sessions.py +1 -1
- gobby/cli/skills.py +858 -0
- gobby/cli/tasks/ai.py +0 -440
- gobby/cli/tasks/crud.py +44 -6
- gobby/cli/tasks/main.py +0 -4
- gobby/cli/tui.py +2 -2
- gobby/cli/utils.py +12 -5
- gobby/clones/__init__.py +13 -0
- gobby/clones/git.py +547 -0
- gobby/conductor/__init__.py +16 -0
- gobby/conductor/alerts.py +135 -0
- gobby/conductor/loop.py +164 -0
- gobby/conductor/monitors/__init__.py +11 -0
- gobby/conductor/monitors/agents.py +116 -0
- gobby/conductor/monitors/tasks.py +155 -0
- gobby/conductor/pricing.py +234 -0
- gobby/conductor/token_tracker.py +160 -0
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +69 -91
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/search.py +110 -0
- gobby/config/servers.py +1 -1
- gobby/config/skills.py +43 -0
- gobby/config/tasks.py +9 -41
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +188 -2
- gobby/hooks/hook_manager.py +50 -4
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/skill_manager.py +130 -0
- gobby/hooks/webhooks.py +1 -1
- gobby/install/claude/hooks/hook_dispatcher.py +4 -4
- gobby/install/codex/hooks/hook_dispatcher.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
- gobby/llm/claude.py +22 -34
- gobby/llm/claude_executor.py +46 -256
- gobby/llm/codex_executor.py +59 -291
- gobby/llm/executor.py +21 -0
- gobby/llm/gemini.py +134 -110
- gobby/llm/litellm_executor.py +143 -6
- gobby/llm/resolver.py +98 -35
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +56 -0
- gobby/mcp_proxy/models.py +15 -0
- gobby/mcp_proxy/registries.py +68 -8
- gobby/mcp_proxy/server.py +33 -3
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/services/tool_proxy.py +81 -1
- gobby/mcp_proxy/stdio.py +2 -1
- gobby/mcp_proxy/tools/__init__.py +0 -2
- gobby/mcp_proxy/tools/agent_messaging.py +317 -0
- gobby/mcp_proxy/tools/agents.py +31 -731
- gobby/mcp_proxy/tools/clones.py +518 -0
- gobby/mcp_proxy/tools/memory.py +3 -26
- gobby/mcp_proxy/tools/metrics.py +65 -1
- gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
- gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
- gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
- gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
- gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
- gobby/mcp_proxy/tools/skills/__init__.py +616 -0
- gobby/mcp_proxy/tools/spawn_agent.py +417 -0
- gobby/mcp_proxy/tools/task_orchestration.py +7 -0
- gobby/mcp_proxy/tools/task_readiness.py +14 -0
- gobby/mcp_proxy/tools/task_sync.py +1 -1
- gobby/mcp_proxy/tools/tasks/_context.py +0 -20
- gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
- gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
- gobby/mcp_proxy/tools/workflows.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +0 -338
- gobby/memory/backends/__init__.py +6 -1
- gobby/memory/backends/mem0.py +6 -1
- gobby/memory/extractor.py +477 -0
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +73 -285
- gobby/memory/search/__init__.py +10 -0
- gobby/memory/search/coordinator.py +248 -0
- gobby/memory/services/__init__.py +5 -0
- gobby/memory/services/crossref.py +142 -0
- gobby/prompts/loader.py +5 -2
- gobby/runner.py +37 -16
- gobby/search/__init__.py +48 -6
- gobby/search/backends/__init__.py +159 -0
- gobby/search/backends/embedding.py +225 -0
- gobby/search/embeddings.py +238 -0
- gobby/search/models.py +148 -0
- gobby/search/unified.py +496 -0
- gobby/servers/http.py +24 -12
- gobby/servers/routes/admin.py +294 -0
- gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
- gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
- gobby/servers/routes/mcp/endpoints/execution.py +568 -0
- gobby/servers/routes/mcp/endpoints/registry.py +378 -0
- gobby/servers/routes/mcp/endpoints/server.py +304 -0
- gobby/servers/routes/mcp/hooks.py +1 -1
- gobby/servers/routes/mcp/tools.py +48 -1317
- gobby/servers/websocket.py +2 -2
- gobby/sessions/analyzer.py +2 -0
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +2 -0
- gobby/sessions/transcripts/claude.py +79 -10
- gobby/skills/__init__.py +91 -0
- gobby/skills/loader.py +685 -0
- gobby/skills/manager.py +384 -0
- gobby/skills/parser.py +286 -0
- gobby/skills/search.py +463 -0
- gobby/skills/sync.py +119 -0
- gobby/skills/updater.py +385 -0
- gobby/skills/validator.py +368 -0
- gobby/storage/clones.py +378 -0
- gobby/storage/database.py +1 -1
- gobby/storage/memories.py +43 -13
- gobby/storage/migrations.py +162 -201
- gobby/storage/sessions.py +116 -7
- gobby/storage/skills.py +782 -0
- gobby/storage/tasks/_crud.py +4 -4
- gobby/storage/tasks/_lifecycle.py +57 -7
- gobby/storage/tasks/_manager.py +14 -5
- gobby/storage/tasks/_models.py +8 -3
- gobby/sync/memories.py +40 -5
- gobby/sync/tasks.py +83 -6
- gobby/tasks/__init__.py +1 -2
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +46 -35
- gobby/tools/summarizer.py +91 -10
- gobby/tui/api_client.py +4 -7
- gobby/tui/app.py +5 -3
- gobby/tui/screens/orchestrator.py +1 -2
- gobby/tui/screens/tasks.py +2 -4
- gobby/tui/ws_client.py +1 -1
- gobby/utils/daemon_client.py +2 -2
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1135
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +93 -1
- gobby/workflows/detection_helpers.py +115 -31
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +269 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
- gobby/workflows/engine.py +13 -2
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/lifecycle_evaluator.py +29 -1
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/loader.py +19 -6
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +154 -0
- gobby/workflows/safe_evaluator.py +183 -0
- gobby/workflows/session_actions.py +44 -0
- gobby/workflows/state_actions.py +60 -1
- gobby/workflows/stop_signal_actions.py +55 -0
- gobby/workflows/summary_actions.py +111 -1
- gobby/workflows/task_sync_actions.py +347 -0
- gobby/workflows/todo_actions.py +34 -1
- gobby/workflows/webhook_actions.py +185 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1292
- gobby/install/claude/commands/gobby/bug.md +0 -51
- gobby/install/claude/commands/gobby/chore.md +0 -51
- gobby/install/claude/commands/gobby/epic.md +0 -52
- gobby/install/claude/commands/gobby/eval.md +0 -235
- gobby/install/claude/commands/gobby/feat.md +0 -49
- gobby/install/claude/commands/gobby/nit.md +0 -52
- gobby/install/claude/commands/gobby/ref.md +0 -52
- gobby/install/codex/prompts/forget.md +0 -7
- gobby/install/codex/prompts/memories.md +0 -7
- gobby/install/codex/prompts/recall.md +0 -7
- gobby/install/codex/prompts/remember.md +0 -13
- gobby/llm/gemini_executor.py +0 -339
- gobby/mcp_proxy/tools/session_messages.py +0 -1056
- gobby/mcp_proxy/tools/task_expansion.py +0 -591
- gobby/prompts/defaults/expansion/system.md +0 -119
- gobby/prompts/defaults/expansion/user.md +0 -48
- gobby/prompts/defaults/external_validation/agent.md +0 -72
- gobby/prompts/defaults/external_validation/external.md +0 -63
- gobby/prompts/defaults/external_validation/spawn.md +0 -83
- gobby/prompts/defaults/external_validation/system.md +0 -6
- gobby/prompts/defaults/features/import_mcp.md +0 -22
- gobby/prompts/defaults/features/import_mcp_github.md +0 -17
- gobby/prompts/defaults/features/import_mcp_search.md +0 -16
- gobby/prompts/defaults/features/recommend_tools.md +0 -32
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
- gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
- gobby/prompts/defaults/features/server_description.md +0 -20
- gobby/prompts/defaults/features/server_description_system.md +0 -6
- gobby/prompts/defaults/features/task_description.md +0 -31
- gobby/prompts/defaults/features/task_description_system.md +0 -6
- gobby/prompts/defaults/features/tool_summary.md +0 -17
- gobby/prompts/defaults/features/tool_summary_system.md +0 -6
- gobby/prompts/defaults/research/step.md +0 -58
- gobby/prompts/defaults/validation/criteria.md +0 -47
- gobby/prompts/defaults/validation/validate.md +0 -38
- gobby/storage/migrations_legacy.py +0 -1359
- gobby/tasks/context.py +0 -747
- gobby/tasks/criteria.py +0 -342
- gobby/tasks/expansion.py +0 -626
- gobby/tasks/prompts/expand.py +0 -327
- gobby/tasks/research.py +0 -421
- gobby/tasks/tdd.py +0 -352
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Search coordination for memory recall operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from gobby.storage.memories import Memory
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from gobby.config.persistence import MemoryConfig
|
|
12
|
+
from gobby.memory.search import SearchBackend
|
|
13
|
+
from gobby.storage.database import DatabaseProtocol
|
|
14
|
+
from gobby.storage.memories import LocalMemoryManager
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SearchCoordinator:
|
|
20
|
+
"""
|
|
21
|
+
Coordinates search operations for memory recall.
|
|
22
|
+
|
|
23
|
+
Manages the search backend lifecycle, fitting, and query execution.
|
|
24
|
+
Extracts search-related logic from MemoryManager for focused responsibility.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
storage: LocalMemoryManager,
|
|
30
|
+
config: MemoryConfig,
|
|
31
|
+
db: DatabaseProtocol,
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
Initialize the search coordinator.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
storage: Memory storage manager for accessing memories
|
|
38
|
+
config: Memory configuration for search settings
|
|
39
|
+
db: Database connection for search backend initialization
|
|
40
|
+
"""
|
|
41
|
+
self._storage = storage
|
|
42
|
+
self._config = config
|
|
43
|
+
self._db = db
|
|
44
|
+
self._search_backend: SearchBackend | None = None
|
|
45
|
+
self._search_backend_fitted = False
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def search_backend(self) -> SearchBackend:
|
|
49
|
+
"""
|
|
50
|
+
Lazy-init search backend based on configuration.
|
|
51
|
+
|
|
52
|
+
The backend type is determined by config.search_backend:
|
|
53
|
+
- "tfidf" (default): Zero-dependency TF-IDF search
|
|
54
|
+
- "text": Simple text substring matching
|
|
55
|
+
"""
|
|
56
|
+
if self._search_backend is None:
|
|
57
|
+
from gobby.memory.search import get_search_backend
|
|
58
|
+
|
|
59
|
+
backend_type = getattr(self._config, "search_backend", "tfidf")
|
|
60
|
+
logger.debug(f"Initializing search backend: {backend_type}")
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
self._search_backend = get_search_backend(
|
|
64
|
+
backend_type=backend_type,
|
|
65
|
+
db=self._db,
|
|
66
|
+
)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.warning(
|
|
69
|
+
f"Failed to initialize {backend_type} backend: {e}. Falling back to tfidf"
|
|
70
|
+
)
|
|
71
|
+
self._search_backend = get_search_backend("tfidf")
|
|
72
|
+
|
|
73
|
+
return self._search_backend
|
|
74
|
+
|
|
75
|
+
def ensure_fitted(self) -> None:
|
|
76
|
+
"""Ensure the search backend is fitted with current memories."""
|
|
77
|
+
if self._search_backend_fitted:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
backend = self.search_backend
|
|
81
|
+
if not backend.needs_refit():
|
|
82
|
+
self._search_backend_fitted = True
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
# Fit the backend with all memories
|
|
86
|
+
max_memories = getattr(self._config, "max_index_memories", 10000)
|
|
87
|
+
memories = self._storage.list_memories(limit=max_memories)
|
|
88
|
+
memory_tuples = [(m.id, m.content) for m in memories]
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
backend.fit(memory_tuples)
|
|
92
|
+
self._search_backend_fitted = True
|
|
93
|
+
logger.info(f"Search backend fitted with {len(memory_tuples)} memories")
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.error(f"Failed to fit search backend: {e}")
|
|
96
|
+
raise
|
|
97
|
+
|
|
98
|
+
def mark_refit_needed(self) -> None:
|
|
99
|
+
"""Mark that the search backend needs to be refitted."""
|
|
100
|
+
self._search_backend_fitted = False
|
|
101
|
+
|
|
102
|
+
def reindex(self) -> dict[str, Any]:
|
|
103
|
+
"""
|
|
104
|
+
Force rebuild of the search index.
|
|
105
|
+
|
|
106
|
+
This method explicitly rebuilds the TF-IDF (or other configured)
|
|
107
|
+
search index from all stored memories. Useful for:
|
|
108
|
+
- Initial index building
|
|
109
|
+
- Recovery after corruption
|
|
110
|
+
- After bulk memory operations
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Dict with index statistics including memory_count, backend_type, etc.
|
|
114
|
+
"""
|
|
115
|
+
# Get all memories
|
|
116
|
+
memories = self._storage.list_memories(limit=10000)
|
|
117
|
+
memory_tuples = [(m.id, m.content) for m in memories]
|
|
118
|
+
|
|
119
|
+
# Force refit the backend
|
|
120
|
+
backend = self.search_backend
|
|
121
|
+
backend_type = getattr(self._config, "search_backend", "tfidf")
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
backend.fit(memory_tuples)
|
|
125
|
+
self._search_backend_fitted = True
|
|
126
|
+
|
|
127
|
+
# Get backend stats
|
|
128
|
+
stats = backend.get_stats() if hasattr(backend, "get_stats") else {}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
"success": True,
|
|
132
|
+
"memory_count": len(memory_tuples),
|
|
133
|
+
"backend_type": backend_type,
|
|
134
|
+
"fitted": True,
|
|
135
|
+
**stats,
|
|
136
|
+
}
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.error(f"Failed to reindex search backend: {e}")
|
|
139
|
+
return {
|
|
140
|
+
"success": False,
|
|
141
|
+
"error": str(e),
|
|
142
|
+
"memory_count": len(memory_tuples),
|
|
143
|
+
"backend_type": backend_type,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
def search(
|
|
147
|
+
self,
|
|
148
|
+
query: str,
|
|
149
|
+
project_id: str | None = None,
|
|
150
|
+
limit: int = 10,
|
|
151
|
+
min_importance: float | None = None,
|
|
152
|
+
search_mode: str | None = None,
|
|
153
|
+
tags_all: list[str] | None = None,
|
|
154
|
+
tags_any: list[str] | None = None,
|
|
155
|
+
tags_none: list[str] | None = None,
|
|
156
|
+
) -> list[Memory]:
|
|
157
|
+
"""
|
|
158
|
+
Perform search using the configured search backend.
|
|
159
|
+
|
|
160
|
+
Uses the new search backend by default (TF-IDF),
|
|
161
|
+
falling back to legacy semantic search if configured.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
query: Search query text
|
|
165
|
+
project_id: Filter by project
|
|
166
|
+
limit: Maximum results to return
|
|
167
|
+
min_importance: Minimum importance threshold
|
|
168
|
+
search_mode: Search mode (tfidf, text, etc.)
|
|
169
|
+
tags_all: Memory must have ALL of these tags
|
|
170
|
+
tags_any: Memory must have at least ONE of these tags
|
|
171
|
+
tags_none: Memory must have NONE of these tags
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
List of matching Memory objects
|
|
175
|
+
"""
|
|
176
|
+
# Determine search mode from config or parameters
|
|
177
|
+
if search_mode is None:
|
|
178
|
+
search_mode = getattr(self._config, "search_backend", "tfidf")
|
|
179
|
+
|
|
180
|
+
# Use the search backend
|
|
181
|
+
try:
|
|
182
|
+
self.ensure_fitted()
|
|
183
|
+
# Fetch more results to allow for filtering
|
|
184
|
+
fetch_multiplier = 3 if (tags_all or tags_any or tags_none) else 2
|
|
185
|
+
results = self.search_backend.search(query, top_k=limit * fetch_multiplier)
|
|
186
|
+
|
|
187
|
+
# Get the actual Memory objects
|
|
188
|
+
memory_ids = [mid for mid, _ in results]
|
|
189
|
+
memories = []
|
|
190
|
+
for mid in memory_ids:
|
|
191
|
+
memory = self._storage.get_memory(mid)
|
|
192
|
+
if memory:
|
|
193
|
+
# Apply filters - allow global memories (project_id is None) to pass through
|
|
194
|
+
if (
|
|
195
|
+
project_id
|
|
196
|
+
and memory.project_id is not None
|
|
197
|
+
and memory.project_id != project_id
|
|
198
|
+
):
|
|
199
|
+
continue
|
|
200
|
+
if min_importance is not None and memory.importance < min_importance:
|
|
201
|
+
continue
|
|
202
|
+
# Apply tag filters
|
|
203
|
+
if not self._passes_tag_filter(memory, tags_all, tags_any, tags_none):
|
|
204
|
+
continue
|
|
205
|
+
memories.append(memory)
|
|
206
|
+
if len(memories) >= limit:
|
|
207
|
+
break
|
|
208
|
+
|
|
209
|
+
return memories
|
|
210
|
+
|
|
211
|
+
except Exception as e:
|
|
212
|
+
logger.warning(f"Search backend failed, falling back to text search: {e}")
|
|
213
|
+
# Fall back to text search with tag filtering
|
|
214
|
+
memories = self._storage.search_memories(
|
|
215
|
+
query_text=query,
|
|
216
|
+
project_id=project_id,
|
|
217
|
+
limit=limit * 2,
|
|
218
|
+
tags_all=tags_all,
|
|
219
|
+
tags_any=tags_any,
|
|
220
|
+
tags_none=tags_none,
|
|
221
|
+
)
|
|
222
|
+
if min_importance:
|
|
223
|
+
memories = [m for m in memories if m.importance >= min_importance]
|
|
224
|
+
return memories[:limit]
|
|
225
|
+
|
|
226
|
+
def _passes_tag_filter(
|
|
227
|
+
self,
|
|
228
|
+
memory: Memory,
|
|
229
|
+
tags_all: list[str] | None = None,
|
|
230
|
+
tags_any: list[str] | None = None,
|
|
231
|
+
tags_none: list[str] | None = None,
|
|
232
|
+
) -> bool:
|
|
233
|
+
"""Check if a memory passes the tag filter criteria."""
|
|
234
|
+
memory_tags = set(memory.tags) if memory.tags else set()
|
|
235
|
+
|
|
236
|
+
# Check tags_all: memory must have ALL specified tags
|
|
237
|
+
if tags_all and not set(tags_all).issubset(memory_tags):
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
# Check tags_any: memory must have at least ONE specified tag
|
|
241
|
+
if tags_any and not memory_tags.intersection(tags_any):
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
# Check tags_none: memory must have NONE of the specified tags
|
|
245
|
+
if tags_none and memory_tags.intersection(tags_none):
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
return True
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Cross-reference service for linking related memories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from gobby.storage.memories import Memory
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from gobby.config.persistence import MemoryConfig
|
|
14
|
+
from gobby.memory.search import SearchBackend
|
|
15
|
+
from gobby.storage.memories import LocalMemoryManager
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CrossrefService:
|
|
21
|
+
"""
|
|
22
|
+
Service for creating and managing cross-references between memories.
|
|
23
|
+
|
|
24
|
+
Cross-references link related memories based on content similarity,
|
|
25
|
+
enabling navigation between conceptually connected items.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
storage: LocalMemoryManager,
|
|
31
|
+
config: MemoryConfig,
|
|
32
|
+
search_backend_getter: Callable[[], SearchBackend],
|
|
33
|
+
ensure_fitted: Callable[[], None],
|
|
34
|
+
):
|
|
35
|
+
"""
|
|
36
|
+
Initialize the cross-reference service.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
storage: Memory storage manager for persistence
|
|
40
|
+
config: Memory configuration for thresholds
|
|
41
|
+
search_backend_getter: Callable that returns the search backend
|
|
42
|
+
ensure_fitted: Callable that ensures search backend is fitted
|
|
43
|
+
"""
|
|
44
|
+
self._storage = storage
|
|
45
|
+
self._config = config
|
|
46
|
+
self._get_search_backend = search_backend_getter
|
|
47
|
+
self._ensure_fitted = ensure_fitted
|
|
48
|
+
|
|
49
|
+
async def create_crossrefs(
|
|
50
|
+
self,
|
|
51
|
+
memory: Memory,
|
|
52
|
+
threshold: float | None = None,
|
|
53
|
+
max_links: int | None = None,
|
|
54
|
+
) -> int:
|
|
55
|
+
"""
|
|
56
|
+
Find and link similar memories.
|
|
57
|
+
|
|
58
|
+
Uses the search backend to find memories similar to the given one
|
|
59
|
+
and creates cross-references for those above the threshold.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
memory: The memory to find links for
|
|
63
|
+
threshold: Minimum similarity to create link (default from config)
|
|
64
|
+
max_links: Maximum links to create (default from config)
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Number of cross-references created
|
|
68
|
+
"""
|
|
69
|
+
# Get thresholds from config or use defaults
|
|
70
|
+
if threshold is None:
|
|
71
|
+
threshold = getattr(self._config, "crossref_threshold", None)
|
|
72
|
+
if threshold is None:
|
|
73
|
+
threshold = 0.3
|
|
74
|
+
if max_links is None:
|
|
75
|
+
max_links = getattr(self._config, "crossref_max_links", None)
|
|
76
|
+
if max_links is None:
|
|
77
|
+
max_links = 5
|
|
78
|
+
|
|
79
|
+
# Ensure search backend is fitted (sync check is fine - just checks a flag)
|
|
80
|
+
self._ensure_fitted()
|
|
81
|
+
|
|
82
|
+
# Search for similar memories (wrap sync I/O in to_thread)
|
|
83
|
+
search_backend = self._get_search_backend()
|
|
84
|
+
similar = await asyncio.to_thread(search_backend.search, memory.content, max_links + 1)
|
|
85
|
+
|
|
86
|
+
# Create cross-references
|
|
87
|
+
created = 0
|
|
88
|
+
for other_id, score in similar:
|
|
89
|
+
# Skip self-reference
|
|
90
|
+
if other_id == memory.id:
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
# Skip below threshold
|
|
94
|
+
if score < threshold:
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
# Create the crossref (wrap sync I/O in to_thread)
|
|
98
|
+
await asyncio.to_thread(self._storage.create_crossref, memory.id, other_id, score)
|
|
99
|
+
created += 1
|
|
100
|
+
|
|
101
|
+
if created >= max_links:
|
|
102
|
+
break
|
|
103
|
+
|
|
104
|
+
if created > 0:
|
|
105
|
+
logger.debug(f"Created {created} crossrefs for memory {memory.id}")
|
|
106
|
+
|
|
107
|
+
return created
|
|
108
|
+
|
|
109
|
+
async def get_related(
|
|
110
|
+
self,
|
|
111
|
+
memory_id: str,
|
|
112
|
+
limit: int = 5,
|
|
113
|
+
min_similarity: float = 0.0,
|
|
114
|
+
) -> list[Memory]:
|
|
115
|
+
"""
|
|
116
|
+
Get memories linked to this one via cross-references.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
memory_id: The memory ID to find related memories for
|
|
120
|
+
limit: Maximum number of results
|
|
121
|
+
min_similarity: Minimum similarity threshold
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
List of related Memory objects, sorted by similarity
|
|
125
|
+
"""
|
|
126
|
+
crossrefs = await asyncio.to_thread(
|
|
127
|
+
self._storage.get_crossrefs,
|
|
128
|
+
memory_id,
|
|
129
|
+
limit=limit,
|
|
130
|
+
min_similarity=min_similarity,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Get the actual Memory objects
|
|
134
|
+
memories = []
|
|
135
|
+
for ref in crossrefs:
|
|
136
|
+
# Get the "other" memory in the relationship
|
|
137
|
+
other_id = ref.target_id if ref.source_id == memory_id else ref.source_id
|
|
138
|
+
memory = await asyncio.to_thread(self._storage.get_memory, other_id)
|
|
139
|
+
if memory:
|
|
140
|
+
memories.append(memory)
|
|
141
|
+
|
|
142
|
+
return memories
|
gobby/prompts/loader.py
CHANGED
|
@@ -23,8 +23,11 @@ from .models import PromptTemplate
|
|
|
23
23
|
|
|
24
24
|
logger = logging.getLogger(__name__)
|
|
25
25
|
|
|
26
|
-
# Default location for bundled prompts
|
|
27
|
-
|
|
26
|
+
# Default location for bundled prompts (in install/shared/prompts for installation)
|
|
27
|
+
# Falls back to src location for development if install location doesn't exist
|
|
28
|
+
_INSTALL_PROMPTS_DIR = Path(__file__).parent.parent / "install" / "shared" / "prompts"
|
|
29
|
+
_DEV_PROMPTS_DIR = Path(__file__).parent / "defaults"
|
|
30
|
+
DEFAULTS_DIR = _INSTALL_PROMPTS_DIR if _INSTALL_PROMPTS_DIR.exists() else _DEV_PROMPTS_DIR
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
class PromptLoader:
|
gobby/runner.py
CHANGED
|
@@ -18,6 +18,7 @@ from gobby.servers.http import HTTPServer
|
|
|
18
18
|
from gobby.servers.websocket import WebSocketConfig, WebSocketServer
|
|
19
19
|
from gobby.sessions.lifecycle import SessionLifecycleManager
|
|
20
20
|
from gobby.sessions.processor import SessionMessageProcessor
|
|
21
|
+
from gobby.storage.clones import LocalCloneManager
|
|
21
22
|
from gobby.storage.database import DatabaseProtocol, LocalDatabase
|
|
22
23
|
from gobby.storage.mcp import LocalMCPManager
|
|
23
24
|
from gobby.storage.migrations import run_migrations
|
|
@@ -28,10 +29,10 @@ from gobby.storage.tasks import LocalTaskManager
|
|
|
28
29
|
from gobby.storage.worktrees import LocalWorktreeManager
|
|
29
30
|
from gobby.sync.memories import MemorySyncManager
|
|
30
31
|
from gobby.sync.tasks import TaskSyncManager
|
|
31
|
-
from gobby.tasks.expansion import TaskExpander
|
|
32
32
|
from gobby.tasks.validation import TaskValidator
|
|
33
33
|
from gobby.utils.logging import setup_file_logging
|
|
34
34
|
from gobby.utils.machine_id import get_machine_id
|
|
35
|
+
from gobby.worktrees.git import WorktreeGitManager
|
|
35
36
|
|
|
36
37
|
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
|
37
38
|
|
|
@@ -43,7 +44,6 @@ class GobbyRunner:
|
|
|
43
44
|
|
|
44
45
|
def __init__(self, config_path: Path | None = None, verbose: bool = False):
|
|
45
46
|
setup_file_logging(verbose=verbose)
|
|
46
|
-
# setup_mcp_logging(verbose=verbose) # Removed as per instruction
|
|
47
47
|
|
|
48
48
|
config_file = str(config_path) if config_path else None
|
|
49
49
|
self.config = load_config(config_file)
|
|
@@ -59,6 +59,16 @@ class GobbyRunner:
|
|
|
59
59
|
self.task_manager = LocalTaskManager(self.database)
|
|
60
60
|
self.session_task_manager = SessionTaskManager(self.database)
|
|
61
61
|
|
|
62
|
+
# Sync bundled skills to database
|
|
63
|
+
from gobby.skills.sync import sync_bundled_skills
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
skill_result = sync_bundled_skills(self.database)
|
|
67
|
+
if skill_result["synced"] > 0:
|
|
68
|
+
logger.info(f"Synced {skill_result['synced']} bundled skills to database")
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.warning(f"Failed to sync bundled skills: {e}")
|
|
71
|
+
|
|
62
72
|
# Initialize LLM Service
|
|
63
73
|
self.llm_service: LLMService | None = None # Added type hint
|
|
64
74
|
try:
|
|
@@ -134,23 +144,11 @@ class GobbyRunner:
|
|
|
134
144
|
poll_interval=self.config.message_tracking.poll_interval,
|
|
135
145
|
)
|
|
136
146
|
|
|
137
|
-
# Initialize Task
|
|
138
|
-
self.task_expander: TaskExpander | None = None
|
|
147
|
+
# Initialize Task Validator (Phase 7.1)
|
|
139
148
|
self.task_validator: TaskValidator | None = None
|
|
140
149
|
|
|
141
150
|
if self.llm_service:
|
|
142
151
|
gobby_tasks_config = self.config.gobby_tasks
|
|
143
|
-
if gobby_tasks_config.expansion.enabled:
|
|
144
|
-
try:
|
|
145
|
-
self.task_expander = TaskExpander(
|
|
146
|
-
llm_service=self.llm_service,
|
|
147
|
-
config=gobby_tasks_config.expansion,
|
|
148
|
-
task_manager=self.task_manager,
|
|
149
|
-
mcp_manager=self.mcp_proxy,
|
|
150
|
-
)
|
|
151
|
-
except Exception as e:
|
|
152
|
-
logger.error(f"Failed to initialize TaskExpander: {e}")
|
|
153
|
-
|
|
154
152
|
if gobby_tasks_config.validation.enabled:
|
|
155
153
|
try:
|
|
156
154
|
self.task_validator = TaskValidator(
|
|
@@ -163,6 +161,26 @@ class GobbyRunner:
|
|
|
163
161
|
# Initialize Worktree Storage (Phase 7 - Subagents)
|
|
164
162
|
self.worktree_storage = LocalWorktreeManager(self.database)
|
|
165
163
|
|
|
164
|
+
# Initialize Clone Storage (local git clones for isolated development)
|
|
165
|
+
self.clone_storage = LocalCloneManager(self.database)
|
|
166
|
+
|
|
167
|
+
# Initialize Git Manager for current project (if in a git repo)
|
|
168
|
+
self.git_manager: WorktreeGitManager | None = None
|
|
169
|
+
self.project_id: str | None = None
|
|
170
|
+
try:
|
|
171
|
+
cwd = Path.cwd()
|
|
172
|
+
project_json = cwd / ".gobby" / "project.json"
|
|
173
|
+
if project_json.exists():
|
|
174
|
+
import json
|
|
175
|
+
|
|
176
|
+
project_data = json.loads(project_json.read_text())
|
|
177
|
+
repo_path = project_data.get("repo_path", str(cwd))
|
|
178
|
+
self.project_id = project_data.get("id")
|
|
179
|
+
self.git_manager = WorktreeGitManager(repo_path)
|
|
180
|
+
logger.info(f"Git manager initialized for project: {self.project_id}")
|
|
181
|
+
except Exception as e:
|
|
182
|
+
logger.debug(f"Could not initialize git manager: {e}")
|
|
183
|
+
|
|
166
184
|
# Initialize Agent Runner (Phase 7 - Subagents)
|
|
167
185
|
# Create executor registry for lazy executor creation
|
|
168
186
|
self.executor_registry = ExecutorRegistry(config=self.config)
|
|
@@ -196,6 +214,7 @@ class GobbyRunner:
|
|
|
196
214
|
# HTTP Server
|
|
197
215
|
self.http_server = HTTPServer(
|
|
198
216
|
port=self.config.daemon_port,
|
|
217
|
+
test_mode=self.config.test_mode,
|
|
199
218
|
mcp_manager=self.mcp_proxy,
|
|
200
219
|
mcp_db_manager=self.mcp_db_manager,
|
|
201
220
|
config=self.config,
|
|
@@ -207,11 +226,13 @@ class GobbyRunner:
|
|
|
207
226
|
llm_service=self.llm_service,
|
|
208
227
|
message_processor=self.message_processor,
|
|
209
228
|
memory_sync_manager=self.memory_sync_manager,
|
|
210
|
-
task_expander=self.task_expander,
|
|
211
229
|
task_validator=self.task_validator,
|
|
212
230
|
metrics_manager=self.metrics_manager,
|
|
213
231
|
agent_runner=self.agent_runner,
|
|
214
232
|
worktree_storage=self.worktree_storage,
|
|
233
|
+
clone_storage=self.clone_storage,
|
|
234
|
+
git_manager=self.git_manager,
|
|
235
|
+
project_id=self.project_id,
|
|
215
236
|
)
|
|
216
237
|
|
|
217
238
|
# Ensure message_processor property is set (redundant but explicit):
|
gobby/search/__init__.py
CHANGED
|
@@ -1,23 +1,65 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Unified search backend abstraction.
|
|
3
3
|
|
|
4
|
-
Provides
|
|
5
|
-
- TF-IDF (default) - Built-in local search using scikit-learn
|
|
4
|
+
Provides a unified search layer with multiple backends:
|
|
5
|
+
- TF-IDF (default) - Built-in local search using scikit-learn
|
|
6
|
+
- Embedding - LiteLLM-based semantic search (OpenAI, Ollama, etc.)
|
|
7
|
+
- Unified - Orchestrates between backends with automatic fallback
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
from gobby.search import
|
|
9
|
+
Basic usage (sync TF-IDF):
|
|
10
|
+
from gobby.search import TFIDFSearcher
|
|
9
11
|
|
|
10
|
-
backend =
|
|
12
|
+
backend = TFIDFSearcher()
|
|
11
13
|
backend.fit([(id, content) for id, content in items])
|
|
12
14
|
results = backend.search("query text", top_k=10)
|
|
15
|
+
|
|
16
|
+
Unified search (async with fallback):
|
|
17
|
+
from gobby.search import UnifiedSearcher, SearchConfig
|
|
18
|
+
|
|
19
|
+
config = SearchConfig(mode="auto") # auto, tfidf, embedding, hybrid
|
|
20
|
+
searcher = UnifiedSearcher(config)
|
|
21
|
+
await searcher.fit_async([(id, content) for id, content in items])
|
|
22
|
+
results = await searcher.search_async("query text", top_k=10)
|
|
23
|
+
|
|
24
|
+
if searcher.is_using_fallback():
|
|
25
|
+
print(f"Using fallback: {searcher.get_fallback_reason()}")
|
|
13
26
|
"""
|
|
14
27
|
|
|
28
|
+
# Sync search backends (backwards compatibility)
|
|
29
|
+
# Async backends
|
|
30
|
+
from gobby.search.backends import AsyncSearchBackend, EmbeddingBackend, TFIDFBackend
|
|
31
|
+
|
|
32
|
+
# Embedding utilities
|
|
33
|
+
from gobby.search.embeddings import (
|
|
34
|
+
generate_embedding,
|
|
35
|
+
generate_embeddings,
|
|
36
|
+
is_embedding_available,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Unified search (async with fallback)
|
|
40
|
+
from gobby.search.models import FallbackEvent, SearchConfig, SearchMode
|
|
15
41
|
from gobby.search.protocol import SearchBackend, SearchResult, get_search_backend
|
|
16
42
|
from gobby.search.tfidf import TFIDFSearcher
|
|
43
|
+
from gobby.search.unified import UnifiedSearcher
|
|
17
44
|
|
|
18
45
|
__all__ = [
|
|
46
|
+
# Sync backends (backwards compatible)
|
|
19
47
|
"SearchBackend",
|
|
20
48
|
"SearchResult",
|
|
21
49
|
"TFIDFSearcher",
|
|
22
50
|
"get_search_backend",
|
|
51
|
+
# Models
|
|
52
|
+
"SearchConfig",
|
|
53
|
+
"SearchMode",
|
|
54
|
+
"FallbackEvent",
|
|
55
|
+
# Unified searcher
|
|
56
|
+
"UnifiedSearcher",
|
|
57
|
+
# Async backends
|
|
58
|
+
"AsyncSearchBackend",
|
|
59
|
+
"TFIDFBackend",
|
|
60
|
+
"EmbeddingBackend",
|
|
61
|
+
# Embedding utilities
|
|
62
|
+
"generate_embedding",
|
|
63
|
+
"generate_embeddings",
|
|
64
|
+
"is_embedding_available",
|
|
23
65
|
]
|