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
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.
|
|
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.
|
|
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
|
-
|
|
49
|
-
self.
|
|
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) ->
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|
|
@@ -178,8 +133,17 @@ class MemoryManager:
|
|
|
178
133
|
source_session_id: Origin session
|
|
179
134
|
tags: Optional tags
|
|
180
135
|
"""
|
|
181
|
-
#
|
|
182
|
-
#
|
|
136
|
+
# Check for existing memory with same content to avoid duplicates.
|
|
137
|
+
# The storage layer also checks via content-hash ID, but this provides
|
|
138
|
+
# an additional safeguard against race conditions and project_id mismatches.
|
|
139
|
+
normalized_content = content.strip()
|
|
140
|
+
if self.storage.content_exists(normalized_content, project_id):
|
|
141
|
+
# Return existing memory by computing the same content-derived ID
|
|
142
|
+
# that the storage layer uses, avoiding reliance on search ordering
|
|
143
|
+
existing_memory = self.storage.get_memory_by_content(normalized_content, project_id)
|
|
144
|
+
if existing_memory:
|
|
145
|
+
logger.debug(f"Memory already exists: {existing_memory.id}")
|
|
146
|
+
return existing_memory
|
|
183
147
|
|
|
184
148
|
memory = self.storage.create_memory(
|
|
185
149
|
content=content,
|
|
@@ -197,7 +161,7 @@ class MemoryManager:
|
|
|
197
161
|
# Auto cross-reference if enabled
|
|
198
162
|
if getattr(self.config, "auto_crossref", False):
|
|
199
163
|
try:
|
|
200
|
-
self.
|
|
164
|
+
await self._crossref_service.create_crossrefs(memory)
|
|
201
165
|
except Exception as e:
|
|
202
166
|
# Don't fail the remember if crossref fails
|
|
203
167
|
logger.warning(f"Auto-crossref failed for {memory.id}: {e}")
|
|
@@ -238,73 +202,19 @@ class MemoryManager:
|
|
|
238
202
|
Raises:
|
|
239
203
|
ValueError: If LLM service is not configured or image not found
|
|
240
204
|
"""
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
# Get LLM provider for image description
|
|
246
|
-
if not self._llm_service:
|
|
247
|
-
raise ValueError(
|
|
248
|
-
"LLM service not configured. Pass llm_service to MemoryManager "
|
|
249
|
-
"to enable remember_with_image."
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
provider = self._llm_service.get_default_provider()
|
|
253
|
-
|
|
254
|
-
# Generate image description
|
|
255
|
-
description = await provider.describe_image(image_path, context=context)
|
|
256
|
-
|
|
257
|
-
# Determine MIME type
|
|
258
|
-
mime_type, _ = mimetypes.guess_type(str(path))
|
|
259
|
-
if not mime_type:
|
|
260
|
-
mime_type = "application/octet-stream"
|
|
261
|
-
|
|
262
|
-
# Create media attachment
|
|
263
|
-
media = MediaAttachment(
|
|
264
|
-
media_type="image",
|
|
265
|
-
content_path=str(path.absolute()),
|
|
266
|
-
mime_type=mime_type,
|
|
267
|
-
description=description,
|
|
268
|
-
description_model=provider.provider_name,
|
|
269
|
-
)
|
|
270
|
-
|
|
271
|
-
# Store memory with media attachment via backend
|
|
272
|
-
record = await self._backend.create(
|
|
273
|
-
content=description,
|
|
205
|
+
memory = await self._multimodal_ingestor.remember_with_image(
|
|
206
|
+
image_path=image_path,
|
|
207
|
+
context=context,
|
|
274
208
|
memory_type=memory_type,
|
|
275
209
|
importance=importance,
|
|
276
210
|
project_id=project_id,
|
|
277
211
|
source_type=source_type,
|
|
278
212
|
source_session_id=source_session_id,
|
|
279
213
|
tags=tags,
|
|
280
|
-
media=[media],
|
|
281
214
|
)
|
|
282
|
-
|
|
283
215
|
# Mark search index for refit
|
|
284
216
|
self.mark_search_refit_needed()
|
|
285
|
-
|
|
286
|
-
# Return as Memory object for backward compatibility
|
|
287
|
-
# Note: The backend returns MemoryRecord, but we need Memory
|
|
288
|
-
memory = self.storage.get_memory(record.id)
|
|
289
|
-
if memory is not None:
|
|
290
|
-
return memory
|
|
291
|
-
|
|
292
|
-
# Fallback: construct Memory from MemoryRecord if storage lookup fails
|
|
293
|
-
# This can happen with synthetic records from failed backend calls
|
|
294
|
-
return Memory(
|
|
295
|
-
id=record.id,
|
|
296
|
-
content=record.content,
|
|
297
|
-
memory_type=record.memory_type,
|
|
298
|
-
created_at=record.created_at.isoformat(),
|
|
299
|
-
updated_at=record.updated_at.isoformat()
|
|
300
|
-
if record.updated_at
|
|
301
|
-
else record.created_at.isoformat(),
|
|
302
|
-
project_id=record.project_id,
|
|
303
|
-
source_type=record.source_type,
|
|
304
|
-
source_session_id=record.source_session_id,
|
|
305
|
-
importance=record.importance,
|
|
306
|
-
tags=record.tags,
|
|
307
|
-
)
|
|
217
|
+
return memory
|
|
308
218
|
|
|
309
219
|
async def remember_screenshot(
|
|
310
220
|
self,
|
|
@@ -339,31 +249,8 @@ class MemoryManager:
|
|
|
339
249
|
Raises:
|
|
340
250
|
ValueError: If LLM service is not configured or screenshot bytes are empty
|
|
341
251
|
"""
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
# Determine resources directory using centralized utility
|
|
346
|
-
from datetime import datetime as dt
|
|
347
|
-
|
|
348
|
-
from gobby.cli.utils import get_resources_dir
|
|
349
|
-
from gobby.utils.project_context import get_project_context
|
|
350
|
-
|
|
351
|
-
ctx = get_project_context()
|
|
352
|
-
project_path = ctx.get("path") if ctx else None
|
|
353
|
-
resources_dir = get_resources_dir(project_path)
|
|
354
|
-
|
|
355
|
-
# Generate timestamp-based filename
|
|
356
|
-
timestamp = dt.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
357
|
-
filename = f"screenshot_{timestamp}.png"
|
|
358
|
-
filepath = resources_dir / filename
|
|
359
|
-
|
|
360
|
-
# Write screenshot to file
|
|
361
|
-
filepath.write_bytes(screenshot_bytes)
|
|
362
|
-
logger.debug(f"Saved screenshot to {filepath}")
|
|
363
|
-
|
|
364
|
-
# Delegate to remember_with_image
|
|
365
|
-
return await self.remember_with_image(
|
|
366
|
-
image_path=str(filepath),
|
|
252
|
+
memory = await self._multimodal_ingestor.remember_screenshot(
|
|
253
|
+
screenshot_bytes=screenshot_bytes,
|
|
367
254
|
context=context,
|
|
368
255
|
memory_type=memory_type,
|
|
369
256
|
importance=importance,
|
|
@@ -372,8 +259,11 @@ class MemoryManager:
|
|
|
372
259
|
source_session_id=source_session_id,
|
|
373
260
|
tags=tags,
|
|
374
261
|
)
|
|
262
|
+
# Mark search index for refit
|
|
263
|
+
self.mark_search_refit_needed()
|
|
264
|
+
return memory
|
|
375
265
|
|
|
376
|
-
def _create_crossrefs(
|
|
266
|
+
async def _create_crossrefs(
|
|
377
267
|
self,
|
|
378
268
|
memory: Memory,
|
|
379
269
|
threshold: float | None = None,
|
|
@@ -393,46 +283,13 @@ class MemoryManager:
|
|
|
393
283
|
Returns:
|
|
394
284
|
Number of cross-references created
|
|
395
285
|
"""
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
threshold
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if max_links is None:
|
|
402
|
-
max_links = getattr(self.config, "crossref_max_links", None)
|
|
403
|
-
if max_links is None:
|
|
404
|
-
max_links = 5
|
|
405
|
-
|
|
406
|
-
# Ensure search backend is fitted
|
|
407
|
-
self._ensure_search_backend_fitted()
|
|
408
|
-
|
|
409
|
-
# Search for similar memories
|
|
410
|
-
similar = self.search_backend.search(memory.content, top_k=max_links + 1)
|
|
411
|
-
|
|
412
|
-
# Create cross-references
|
|
413
|
-
created = 0
|
|
414
|
-
for other_id, score in similar:
|
|
415
|
-
# Skip self-reference
|
|
416
|
-
if other_id == memory.id:
|
|
417
|
-
continue
|
|
418
|
-
|
|
419
|
-
# Skip below threshold
|
|
420
|
-
if score < threshold:
|
|
421
|
-
continue
|
|
422
|
-
|
|
423
|
-
# Create the crossref
|
|
424
|
-
self.storage.create_crossref(memory.id, other_id, score)
|
|
425
|
-
created += 1
|
|
426
|
-
|
|
427
|
-
if created >= max_links:
|
|
428
|
-
break
|
|
429
|
-
|
|
430
|
-
if created > 0:
|
|
431
|
-
logger.debug(f"Created {created} crossrefs for memory {memory.id}")
|
|
432
|
-
|
|
433
|
-
return created
|
|
286
|
+
return await self._crossref_service.create_crossrefs(
|
|
287
|
+
memory=memory,
|
|
288
|
+
threshold=threshold,
|
|
289
|
+
max_links=max_links,
|
|
290
|
+
)
|
|
434
291
|
|
|
435
|
-
def get_related(
|
|
292
|
+
async def get_related(
|
|
436
293
|
self,
|
|
437
294
|
memory_id: str,
|
|
438
295
|
limit: int = 5,
|
|
@@ -449,21 +306,12 @@ class MemoryManager:
|
|
|
449
306
|
Returns:
|
|
450
307
|
List of related Memory objects, sorted by similarity
|
|
451
308
|
"""
|
|
452
|
-
|
|
453
|
-
memory_id
|
|
309
|
+
return await self._crossref_service.get_related(
|
|
310
|
+
memory_id=memory_id,
|
|
311
|
+
limit=limit,
|
|
312
|
+
min_similarity=min_similarity,
|
|
454
313
|
)
|
|
455
314
|
|
|
456
|
-
# Get the actual Memory objects
|
|
457
|
-
memories = []
|
|
458
|
-
for ref in crossrefs:
|
|
459
|
-
# Get the "other" memory in the relationship
|
|
460
|
-
other_id = ref.target_id if ref.source_id == memory_id else ref.source_id
|
|
461
|
-
memory = self.get_memory(other_id)
|
|
462
|
-
if memory:
|
|
463
|
-
memories.append(memory)
|
|
464
|
-
|
|
465
|
-
return memories
|
|
466
|
-
|
|
467
315
|
def recall(
|
|
468
316
|
self,
|
|
469
317
|
query: str | None = None,
|
|
@@ -546,80 +394,20 @@ class MemoryManager:
|
|
|
546
394
|
Uses the new search backend by default (TF-IDF),
|
|
547
395
|
falling back to legacy semantic search if configured.
|
|
548
396
|
"""
|
|
549
|
-
# Determine search mode from config or parameters
|
|
550
|
-
if search_mode is None:
|
|
551
|
-
search_mode = getattr(self.config, "search_backend", "tfidf")
|
|
552
|
-
|
|
553
397
|
# Legacy compatibility: use_semantic is deprecated
|
|
554
398
|
if use_semantic is not None:
|
|
555
399
|
logger.warning("use_semantic argument is deprecated and ignored")
|
|
556
400
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
for mid in memory_ids:
|
|
568
|
-
memory = self.get_memory(mid)
|
|
569
|
-
if memory:
|
|
570
|
-
# Apply filters
|
|
571
|
-
if project_id and memory.project_id != project_id:
|
|
572
|
-
if memory.project_id is not None: # Allow global memories
|
|
573
|
-
continue
|
|
574
|
-
if min_importance and memory.importance < min_importance:
|
|
575
|
-
continue
|
|
576
|
-
# Apply tag filters
|
|
577
|
-
if not self._passes_tag_filter(memory, tags_all, tags_any, tags_none):
|
|
578
|
-
continue
|
|
579
|
-
memories.append(memory)
|
|
580
|
-
if len(memories) >= limit:
|
|
581
|
-
break
|
|
582
|
-
|
|
583
|
-
return memories
|
|
584
|
-
|
|
585
|
-
except Exception as e:
|
|
586
|
-
logger.warning(f"Search backend failed, falling back to text search: {e}")
|
|
587
|
-
# Fall back to text search with tag filtering
|
|
588
|
-
memories = self.storage.search_memories(
|
|
589
|
-
query_text=query,
|
|
590
|
-
project_id=project_id,
|
|
591
|
-
limit=limit * 2,
|
|
592
|
-
tags_all=tags_all,
|
|
593
|
-
tags_any=tags_any,
|
|
594
|
-
tags_none=tags_none,
|
|
595
|
-
)
|
|
596
|
-
if min_importance:
|
|
597
|
-
memories = [m for m in memories if m.importance >= min_importance]
|
|
598
|
-
return memories[:limit]
|
|
599
|
-
|
|
600
|
-
def _passes_tag_filter(
|
|
601
|
-
self,
|
|
602
|
-
memory: Memory,
|
|
603
|
-
tags_all: list[str] | None = None,
|
|
604
|
-
tags_any: list[str] | None = None,
|
|
605
|
-
tags_none: list[str] | None = None,
|
|
606
|
-
) -> bool:
|
|
607
|
-
"""Check if a memory passes the tag filter criteria."""
|
|
608
|
-
memory_tags = set(memory.tags) if memory.tags else set()
|
|
609
|
-
|
|
610
|
-
# Check tags_all: memory must have ALL specified tags
|
|
611
|
-
if tags_all and not set(tags_all).issubset(memory_tags):
|
|
612
|
-
return False
|
|
613
|
-
|
|
614
|
-
# Check tags_any: memory must have at least ONE specified tag
|
|
615
|
-
if tags_any and not memory_tags.intersection(tags_any):
|
|
616
|
-
return False
|
|
617
|
-
|
|
618
|
-
# Check tags_none: memory must have NONE of the specified tags
|
|
619
|
-
if tags_none and memory_tags.intersection(tags_none):
|
|
620
|
-
return False
|
|
621
|
-
|
|
622
|
-
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
|
+
)
|
|
623
411
|
|
|
624
412
|
def recall_as_context(
|
|
625
413
|
self,
|
gobby/memory/search/__init__.py
CHANGED
|
@@ -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,
|