gobby 0.2.6__py3-none-any.whl → 0.2.8__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 +96 -35
- 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/adapters/gemini.py +140 -38
- gobby/agents/definitions.py +11 -1
- gobby/agents/isolation.py +525 -0
- gobby/agents/registry.py +11 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/session.py +1 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +415 -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/macos.py +26 -1
- gobby/agents/spawners/prompt_manager.py +125 -0
- gobby/cli/__init__.py +0 -2
- gobby/cli/install.py +4 -4
- gobby/cli/installers/claude.py +6 -0
- gobby/cli/installers/gemini.py +6 -0
- gobby/cli/installers/shared.py +103 -4
- gobby/cli/memory.py +185 -0
- gobby/cli/sessions.py +1 -1
- gobby/cli/utils.py +9 -2
- gobby/clones/git.py +177 -0
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +10 -94
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/skills.py +31 -0
- gobby/config/tasks.py +4 -28
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +150 -8
- gobby/hooks/hook_manager.py +21 -3
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/webhooks.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
- gobby/llm/resolver.py +3 -2
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +4 -2
- gobby/mcp_proxy/registries.py +22 -8
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/tools/agent_messaging.py +93 -44
- gobby/mcp_proxy/tools/agents.py +76 -740
- gobby/mcp_proxy/tools/artifacts.py +43 -9
- gobby/mcp_proxy/tools/clones.py +0 -385
- gobby/mcp_proxy/tools/memory.py +2 -2
- gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +239 -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 +503 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +166 -0
- gobby/mcp_proxy/tools/skills/__init__.py +14 -29
- gobby/mcp_proxy/tools/spawn_agent.py +455 -0
- gobby/mcp_proxy/tools/tasks/_context.py +18 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +79 -30
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
- gobby/mcp_proxy/tools/tasks/_session.py +22 -7
- gobby/mcp_proxy/tools/workflows.py +84 -34
- gobby/mcp_proxy/tools/worktrees.py +32 -350
- gobby/memory/extractor.py +15 -1
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +62 -283
- 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 +13 -0
- gobby/servers/http.py +1 -4
- gobby/servers/routes/admin.py +14 -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 +51 -4
- gobby/servers/routes/mcp/tools.py +48 -1506
- gobby/servers/websocket.py +57 -1
- gobby/sessions/analyzer.py +2 -2
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/manager.py +9 -0
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +1 -0
- gobby/sessions/transcripts/claude.py +15 -5
- gobby/sessions/transcripts/gemini.py +100 -34
- gobby/skills/parser.py +30 -2
- gobby/storage/database.py +9 -2
- gobby/storage/memories.py +32 -21
- gobby/storage/migrations.py +174 -368
- gobby/storage/sessions.py +45 -7
- gobby/storage/skills.py +80 -7
- gobby/storage/tasks/_lifecycle.py +18 -3
- gobby/sync/memories.py +1 -1
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +22 -20
- gobby/tools/summarizer.py +91 -10
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1217
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +50 -1
- gobby/workflows/detection_helpers.py +38 -24
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +281 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/enforcement/task_policy.py +542 -0
- gobby/workflows/engine.py +93 -0
- gobby/workflows/evaluator.py +110 -0
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/hooks.py +41 -0
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +91 -0
- gobby/workflows/safe_evaluator.py +191 -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 +217 -51
- 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.6.dist-info → gobby-0.2.8.dist-info}/METADATA +6 -1
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/RECORD +139 -163
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1332
- gobby/cli/tui.py +0 -34
- 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/mcp_proxy/tools/session_messages.py +0 -1055
- 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/handoff/compact.md +0 -63
- gobby/prompts/defaults/handoff/session_end.md +0 -57
- gobby/prompts/defaults/memory/extract.md +0 -61
- 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/tui/__init__.py +0 -5
- gobby/tui/api_client.py +0 -278
- gobby/tui/app.py +0 -329
- gobby/tui/screens/__init__.py +0 -25
- gobby/tui/screens/agents.py +0 -333
- gobby/tui/screens/chat.py +0 -450
- gobby/tui/screens/dashboard.py +0 -377
- gobby/tui/screens/memory.py +0 -305
- gobby/tui/screens/metrics.py +0 -231
- gobby/tui/screens/orchestrator.py +0 -903
- gobby/tui/screens/sessions.py +0 -412
- gobby/tui/screens/tasks.py +0 -440
- gobby/tui/screens/workflows.py +0 -289
- gobby/tui/screens/worktrees.py +0 -174
- gobby/tui/widgets/__init__.py +0 -21
- gobby/tui/widgets/chat.py +0 -210
- gobby/tui/widgets/conductor.py +0 -104
- gobby/tui/widgets/menu.py +0 -132
- gobby/tui/widgets/message_panel.py +0 -160
- gobby/tui/widgets/review_gate.py +0 -224
- gobby/tui/widgets/task_tree.py +0 -99
- gobby/tui/widgets/token_budget.py +0 -166
- gobby/tui/ws_client.py +0 -258
- gobby/workflows/task_enforcement_actions.py +0 -1343
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.8.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
|
@@ -471,6 +471,19 @@ class GobbyRunner:
|
|
|
471
471
|
except (asyncio.CancelledError, TimeoutError):
|
|
472
472
|
pass
|
|
473
473
|
|
|
474
|
+
# Export memories to JSONL backup on shutdown
|
|
475
|
+
if self.memory_sync_manager:
|
|
476
|
+
try:
|
|
477
|
+
count = await asyncio.wait_for(
|
|
478
|
+
self.memory_sync_manager.export_to_files(), timeout=5.0
|
|
479
|
+
)
|
|
480
|
+
if count > 0:
|
|
481
|
+
logger.info(f"Shutdown memory backup: exported {count} memories")
|
|
482
|
+
except TimeoutError:
|
|
483
|
+
logger.warning("Memory backup on shutdown timed out")
|
|
484
|
+
except Exception as e:
|
|
485
|
+
logger.warning(f"Memory backup on shutdown failed: {e}")
|
|
486
|
+
|
|
474
487
|
try:
|
|
475
488
|
await asyncio.wait_for(self.mcp_proxy.disconnect_all(), timeout=3.0)
|
|
476
489
|
except TimeoutError:
|
gobby/servers/http.py
CHANGED
|
@@ -16,7 +16,7 @@ from fastapi import FastAPI, HTTPException, Request
|
|
|
16
16
|
from fastapi.middleware.cors import CORSMiddleware
|
|
17
17
|
from fastapi.responses import JSONResponse
|
|
18
18
|
|
|
19
|
-
from gobby.adapters.
|
|
19
|
+
from gobby.adapters.codex_impl.adapter import CodexAdapter
|
|
20
20
|
from gobby.hooks.broadcaster import HookEventBroadcaster
|
|
21
21
|
from gobby.hooks.hook_manager import HookManager
|
|
22
22
|
from gobby.llm import LLMService, create_llm_service
|
|
@@ -25,9 +25,6 @@ from gobby.mcp_proxy.semantic_search import SemanticToolSearch
|
|
|
25
25
|
from gobby.mcp_proxy.server import GobbyDaemonTools, create_mcp_server
|
|
26
26
|
from gobby.mcp_proxy.services.tool_filter import ToolFilterService
|
|
27
27
|
from gobby.memory.manager import MemoryManager
|
|
28
|
-
|
|
29
|
-
# Re-export for backward compatibility
|
|
30
|
-
from gobby.servers.models import SessionRegisterRequest # noqa: F401
|
|
31
28
|
from gobby.storage.sessions import LocalSessionManager
|
|
32
29
|
from gobby.storage.tasks import LocalTaskManager
|
|
33
30
|
from gobby.sync.tasks import TaskSyncManager
|
gobby/servers/routes/admin.py
CHANGED
|
@@ -175,6 +175,19 @@ def create_admin_router(server: "HTTPServer") -> APIRouter:
|
|
|
175
175
|
except Exception as e:
|
|
176
176
|
logger.warning(f"Failed to get memory stats: {e}")
|
|
177
177
|
|
|
178
|
+
# Get skills statistics
|
|
179
|
+
skills_stats: dict[str, Any] = {"total": 0}
|
|
180
|
+
if server._internal_manager:
|
|
181
|
+
try:
|
|
182
|
+
for registry in server._internal_manager.get_all_registries():
|
|
183
|
+
if registry.name == "gobby-skills":
|
|
184
|
+
result = await registry.call("list_skills", {"limit": 10000})
|
|
185
|
+
if result.get("success"):
|
|
186
|
+
skills_stats["total"] = result.get("count", 0)
|
|
187
|
+
break
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.warning(f"Failed to get skills stats: {e}")
|
|
190
|
+
|
|
178
191
|
# Get plugin status
|
|
179
192
|
plugin_stats: dict[str, Any] = {"enabled": False, "loaded": 0, "handlers": 0}
|
|
180
193
|
if hasattr(server, "_hook_manager") and server._hook_manager is not None:
|
|
@@ -218,6 +231,7 @@ def create_admin_router(server: "HTTPServer") -> APIRouter:
|
|
|
218
231
|
"sessions": session_stats,
|
|
219
232
|
"tasks": task_stats,
|
|
220
233
|
"memory": memory_stats,
|
|
234
|
+
"skills": skills_stats,
|
|
221
235
|
"plugins": plugin_stats,
|
|
222
236
|
"response_time_ms": response_time_ms,
|
|
223
237
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP endpoint modules for the Gobby HTTP server.
|
|
3
|
+
|
|
4
|
+
This package contains decomposed endpoint handlers extracted from tools.py
|
|
5
|
+
using the Strangler Fig pattern. Each module handles a specific domain:
|
|
6
|
+
|
|
7
|
+
- discovery: Tool and server listing endpoints
|
|
8
|
+
- execution: Tool invocation endpoints
|
|
9
|
+
- server: Server management (add/remove/import)
|
|
10
|
+
- registry: Tool embedding, status, and refresh
|
|
11
|
+
|
|
12
|
+
External modules should import `create_mcp_router` from the parent package:
|
|
13
|
+
from gobby.servers.routes.mcp import create_mcp_router
|
|
14
|
+
|
|
15
|
+
For direct endpoint access (e.g., testing), import from submodules:
|
|
16
|
+
from gobby.servers.routes.mcp.endpoints.discovery import list_all_mcp_tools
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from gobby.servers.routes.mcp.endpoints.discovery import (
|
|
20
|
+
list_all_mcp_tools,
|
|
21
|
+
recommend_mcp_tools,
|
|
22
|
+
search_mcp_tools,
|
|
23
|
+
)
|
|
24
|
+
from gobby.servers.routes.mcp.endpoints.execution import (
|
|
25
|
+
call_mcp_tool,
|
|
26
|
+
get_tool_schema,
|
|
27
|
+
list_mcp_tools,
|
|
28
|
+
mcp_proxy,
|
|
29
|
+
)
|
|
30
|
+
from gobby.servers.routes.mcp.endpoints.registry import (
|
|
31
|
+
embed_mcp_tools,
|
|
32
|
+
get_mcp_status,
|
|
33
|
+
refresh_mcp_tools,
|
|
34
|
+
)
|
|
35
|
+
from gobby.servers.routes.mcp.endpoints.server import (
|
|
36
|
+
add_mcp_server,
|
|
37
|
+
import_mcp_server,
|
|
38
|
+
list_mcp_servers,
|
|
39
|
+
remove_mcp_server,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
# Discovery
|
|
44
|
+
"list_all_mcp_tools",
|
|
45
|
+
"recommend_mcp_tools",
|
|
46
|
+
"search_mcp_tools",
|
|
47
|
+
# Execution
|
|
48
|
+
"call_mcp_tool",
|
|
49
|
+
"get_tool_schema",
|
|
50
|
+
"list_mcp_tools",
|
|
51
|
+
"mcp_proxy",
|
|
52
|
+
# Registry
|
|
53
|
+
"embed_mcp_tools",
|
|
54
|
+
"get_mcp_status",
|
|
55
|
+
"refresh_mcp_tools",
|
|
56
|
+
# Server
|
|
57
|
+
"add_mcp_server",
|
|
58
|
+
"import_mcp_server",
|
|
59
|
+
"list_mcp_servers",
|
|
60
|
+
"remove_mcp_server",
|
|
61
|
+
]
|