gobby 0.2.5__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 +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
gobby/runner.py
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import signal
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import uvicorn
|
|
10
|
+
|
|
11
|
+
from gobby.agents.runner import AgentRunner
|
|
12
|
+
from gobby.config.app import load_config
|
|
13
|
+
from gobby.llm import LLMService, create_llm_service
|
|
14
|
+
from gobby.llm.resolver import ExecutorRegistry
|
|
15
|
+
from gobby.mcp_proxy.manager import MCPClientManager
|
|
16
|
+
from gobby.memory.manager import MemoryManager
|
|
17
|
+
from gobby.servers.http import HTTPServer
|
|
18
|
+
from gobby.servers.websocket import WebSocketConfig, WebSocketServer
|
|
19
|
+
from gobby.sessions.lifecycle import SessionLifecycleManager
|
|
20
|
+
from gobby.sessions.processor import SessionMessageProcessor
|
|
21
|
+
from gobby.storage.database import DatabaseProtocol, LocalDatabase
|
|
22
|
+
from gobby.storage.mcp import LocalMCPManager
|
|
23
|
+
from gobby.storage.migrations import run_migrations
|
|
24
|
+
from gobby.storage.session_messages import LocalSessionMessageManager
|
|
25
|
+
from gobby.storage.session_tasks import SessionTaskManager
|
|
26
|
+
from gobby.storage.sessions import LocalSessionManager
|
|
27
|
+
from gobby.storage.tasks import LocalTaskManager
|
|
28
|
+
from gobby.storage.worktrees import LocalWorktreeManager
|
|
29
|
+
from gobby.sync.memories import MemorySyncManager
|
|
30
|
+
from gobby.sync.tasks import TaskSyncManager
|
|
31
|
+
from gobby.tasks.expansion import TaskExpander
|
|
32
|
+
from gobby.tasks.validation import TaskValidator
|
|
33
|
+
from gobby.utils.logging import setup_file_logging
|
|
34
|
+
from gobby.utils.machine_id import get_machine_id
|
|
35
|
+
|
|
36
|
+
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class GobbyRunner:
|
|
42
|
+
"""Runner for Gobby daemon."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, config_path: Path | None = None, verbose: bool = False):
|
|
45
|
+
setup_file_logging(verbose=verbose)
|
|
46
|
+
# setup_mcp_logging(verbose=verbose) # Removed as per instruction
|
|
47
|
+
|
|
48
|
+
config_file = str(config_path) if config_path else None
|
|
49
|
+
self.config = load_config(config_file)
|
|
50
|
+
self.verbose = verbose
|
|
51
|
+
self.machine_id = get_machine_id()
|
|
52
|
+
self._shutdown_requested = False
|
|
53
|
+
self._metrics_cleanup_task: asyncio.Task[None] | None = None
|
|
54
|
+
|
|
55
|
+
# Initialize local storage with dual-write if in project context
|
|
56
|
+
self.database = self._init_database()
|
|
57
|
+
self.session_manager = LocalSessionManager(self.database)
|
|
58
|
+
self.message_manager = LocalSessionMessageManager(self.database)
|
|
59
|
+
self.task_manager = LocalTaskManager(self.database)
|
|
60
|
+
self.session_task_manager = SessionTaskManager(self.database)
|
|
61
|
+
|
|
62
|
+
# Initialize LLM Service
|
|
63
|
+
self.llm_service: LLMService | None = None # Added type hint
|
|
64
|
+
try:
|
|
65
|
+
self.llm_service = create_llm_service(self.config)
|
|
66
|
+
logger.debug(f"LLM service initialized: {self.llm_service.enabled_providers}")
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error(f"Failed to initialize LLM service: {e}")
|
|
69
|
+
|
|
70
|
+
# Initialize Memory Manager
|
|
71
|
+
self.memory_manager: MemoryManager | None = None
|
|
72
|
+
if hasattr(self.config, "memory"):
|
|
73
|
+
try:
|
|
74
|
+
self.memory_manager = MemoryManager(
|
|
75
|
+
self.database,
|
|
76
|
+
self.config.memory,
|
|
77
|
+
)
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error(f"Failed to initialize MemoryManager: {e}")
|
|
80
|
+
|
|
81
|
+
# MCP Proxy Manager - Initialize early for tool access
|
|
82
|
+
# LocalMCPManager handles server/tool storage in SQLite
|
|
83
|
+
self.mcp_db_manager = LocalMCPManager(self.database)
|
|
84
|
+
|
|
85
|
+
# Tool Metrics Manager for tracking call statistics
|
|
86
|
+
from gobby.mcp_proxy.metrics import ToolMetricsManager
|
|
87
|
+
|
|
88
|
+
self.metrics_manager = ToolMetricsManager(self.database)
|
|
89
|
+
|
|
90
|
+
# MCPClientManager loads servers from database on init
|
|
91
|
+
self.mcp_proxy = MCPClientManager(
|
|
92
|
+
mcp_db_manager=self.mcp_db_manager,
|
|
93
|
+
metrics_manager=self.metrics_manager,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Task Sync Manager
|
|
97
|
+
self.task_sync_manager = TaskSyncManager(self.task_manager)
|
|
98
|
+
# Wire up change listener for automatic export
|
|
99
|
+
self.task_manager.add_change_listener(self.task_sync_manager.trigger_export)
|
|
100
|
+
|
|
101
|
+
# Initialize Memory Sync Manager (Phase 7) & Wire up listeners
|
|
102
|
+
self.memory_sync_manager: MemorySyncManager | None = None
|
|
103
|
+
if hasattr(self.config, "memory_sync") and self.config.memory_sync.enabled:
|
|
104
|
+
if self.memory_manager:
|
|
105
|
+
try:
|
|
106
|
+
self.memory_sync_manager = MemorySyncManager(
|
|
107
|
+
db=self.database,
|
|
108
|
+
memory_manager=self.memory_manager,
|
|
109
|
+
config=self.config.memory_sync,
|
|
110
|
+
)
|
|
111
|
+
# Wire up listener to trigger export on changes
|
|
112
|
+
self.memory_manager.storage.add_change_listener(
|
|
113
|
+
self.memory_sync_manager.trigger_export
|
|
114
|
+
)
|
|
115
|
+
logger.debug("MemorySyncManager initialized and listener attached")
|
|
116
|
+
|
|
117
|
+
# Force initial synchronous export
|
|
118
|
+
# Ensures disk state matches DB state before we start serving
|
|
119
|
+
try:
|
|
120
|
+
self.memory_sync_manager.export_sync()
|
|
121
|
+
logger.info("Initial memory sync export completed")
|
|
122
|
+
except Exception as e:
|
|
123
|
+
logger.warning(f"Initial memory sync failed: {e}")
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
logger.error(f"Failed to initialize MemorySyncManager: {e}")
|
|
127
|
+
|
|
128
|
+
# Session Message Processor (Phase 6)
|
|
129
|
+
# Created here and passed to HTTPServer which injects it into HookManager
|
|
130
|
+
self.message_processor: SessionMessageProcessor | None = None
|
|
131
|
+
if getattr(self.config, "message_tracking", None) and self.config.message_tracking.enabled:
|
|
132
|
+
self.message_processor = SessionMessageProcessor(
|
|
133
|
+
db=self.database,
|
|
134
|
+
poll_interval=self.config.message_tracking.poll_interval,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Initialize Task Managers (Phase 7.1)
|
|
138
|
+
self.task_expander: TaskExpander | None = None
|
|
139
|
+
self.task_validator: TaskValidator | None = None
|
|
140
|
+
|
|
141
|
+
if self.llm_service:
|
|
142
|
+
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
|
+
if gobby_tasks_config.validation.enabled:
|
|
155
|
+
try:
|
|
156
|
+
self.task_validator = TaskValidator(
|
|
157
|
+
llm_service=self.llm_service,
|
|
158
|
+
config=gobby_tasks_config.validation,
|
|
159
|
+
)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.error(f"Failed to initialize TaskValidator: {e}")
|
|
162
|
+
|
|
163
|
+
# Initialize Worktree Storage (Phase 7 - Subagents)
|
|
164
|
+
self.worktree_storage = LocalWorktreeManager(self.database)
|
|
165
|
+
|
|
166
|
+
# Initialize Agent Runner (Phase 7 - Subagents)
|
|
167
|
+
# Create executor registry for lazy executor creation
|
|
168
|
+
self.executor_registry = ExecutorRegistry(config=self.config)
|
|
169
|
+
self.agent_runner: AgentRunner | None = None
|
|
170
|
+
try:
|
|
171
|
+
# Pre-initialize common executors
|
|
172
|
+
executors = {}
|
|
173
|
+
for provider in ["claude", "gemini", "litellm"]:
|
|
174
|
+
try:
|
|
175
|
+
executors[provider] = self.executor_registry.get(provider=provider)
|
|
176
|
+
logger.info(f"Pre-initialized {provider} executor")
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.debug(f"Could not pre-initialize {provider} executor: {e}")
|
|
179
|
+
|
|
180
|
+
self.agent_runner = AgentRunner(
|
|
181
|
+
db=self.database,
|
|
182
|
+
session_storage=self.session_manager,
|
|
183
|
+
executors=executors,
|
|
184
|
+
max_agent_depth=3,
|
|
185
|
+
)
|
|
186
|
+
logger.debug(f"AgentRunner initialized with executors: {list(executors.keys())}")
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.error(f"Failed to initialize AgentRunner: {e}")
|
|
189
|
+
|
|
190
|
+
# Session Lifecycle Manager (background jobs for expiring and processing)
|
|
191
|
+
self.lifecycle_manager = SessionLifecycleManager(
|
|
192
|
+
db=self.database,
|
|
193
|
+
config=self.config.session_lifecycle,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# HTTP Server
|
|
197
|
+
self.http_server = HTTPServer(
|
|
198
|
+
port=self.config.daemon_port,
|
|
199
|
+
mcp_manager=self.mcp_proxy,
|
|
200
|
+
mcp_db_manager=self.mcp_db_manager,
|
|
201
|
+
config=self.config,
|
|
202
|
+
session_manager=self.session_manager,
|
|
203
|
+
task_manager=self.task_manager,
|
|
204
|
+
task_sync_manager=self.task_sync_manager,
|
|
205
|
+
message_manager=self.message_manager,
|
|
206
|
+
memory_manager=self.memory_manager,
|
|
207
|
+
llm_service=self.llm_service,
|
|
208
|
+
message_processor=self.message_processor,
|
|
209
|
+
memory_sync_manager=self.memory_sync_manager,
|
|
210
|
+
task_expander=self.task_expander,
|
|
211
|
+
task_validator=self.task_validator,
|
|
212
|
+
metrics_manager=self.metrics_manager,
|
|
213
|
+
agent_runner=self.agent_runner,
|
|
214
|
+
worktree_storage=self.worktree_storage,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Ensure message_processor property is set (redundant but explicit):
|
|
218
|
+
self.http_server.message_processor = self.message_processor
|
|
219
|
+
|
|
220
|
+
# WebSocket Server (Optional)
|
|
221
|
+
self.websocket_server: WebSocketServer | None = None
|
|
222
|
+
if self.config.websocket and getattr(self.config.websocket, "enabled", True):
|
|
223
|
+
websocket_config = WebSocketConfig(
|
|
224
|
+
host="localhost",
|
|
225
|
+
port=self.config.websocket.port,
|
|
226
|
+
ping_interval=self.config.websocket.ping_interval,
|
|
227
|
+
ping_timeout=self.config.websocket.ping_timeout,
|
|
228
|
+
)
|
|
229
|
+
self.websocket_server = WebSocketServer(
|
|
230
|
+
config=websocket_config,
|
|
231
|
+
mcp_manager=self.mcp_proxy,
|
|
232
|
+
)
|
|
233
|
+
# Pass WebSocket server reference to HTTP server for broadcasting
|
|
234
|
+
self.http_server.websocket_server = self.websocket_server
|
|
235
|
+
# Also update the HTTPServer's broadcaster to use the same websocket_server
|
|
236
|
+
self.http_server.broadcaster.websocket_server = self.websocket_server
|
|
237
|
+
|
|
238
|
+
# Pass WebSocket server to message processor if enabled
|
|
239
|
+
if self.message_processor:
|
|
240
|
+
self.message_processor.websocket_server = self.websocket_server
|
|
241
|
+
|
|
242
|
+
# Register agent event callback for WebSocket broadcasting
|
|
243
|
+
self._setup_agent_event_broadcasting()
|
|
244
|
+
|
|
245
|
+
def _init_database(self) -> DatabaseProtocol:
|
|
246
|
+
"""Initialize hub database."""
|
|
247
|
+
hub_db_path = Path(self.config.database_path).expanduser()
|
|
248
|
+
|
|
249
|
+
# Ensure hub db directory exists
|
|
250
|
+
hub_db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
251
|
+
|
|
252
|
+
hub_db = LocalDatabase(hub_db_path)
|
|
253
|
+
run_migrations(hub_db)
|
|
254
|
+
|
|
255
|
+
logger.info(f"Database: {hub_db_path}")
|
|
256
|
+
return hub_db
|
|
257
|
+
|
|
258
|
+
def _setup_agent_event_broadcasting(self) -> None:
|
|
259
|
+
"""Set up WebSocket broadcasting for agent lifecycle events."""
|
|
260
|
+
from gobby.agents.registry import get_running_agent_registry
|
|
261
|
+
|
|
262
|
+
if not self.websocket_server:
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
registry = get_running_agent_registry()
|
|
266
|
+
|
|
267
|
+
def broadcast_agent_event(event_type: str, run_id: str, data: dict[str, Any]) -> None:
|
|
268
|
+
"""Broadcast agent events via WebSocket (non-blocking)."""
|
|
269
|
+
if not self.websocket_server:
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
def _log_broadcast_exception(task: asyncio.Task[None]) -> None:
|
|
273
|
+
"""Log exceptions from broadcast task to avoid silent failures."""
|
|
274
|
+
try:
|
|
275
|
+
task.result()
|
|
276
|
+
except asyncio.CancelledError:
|
|
277
|
+
pass
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.warning(f"Failed to broadcast agent event {event_type}: {e}")
|
|
280
|
+
|
|
281
|
+
# Create async task to broadcast and attach exception callback
|
|
282
|
+
task = asyncio.create_task(
|
|
283
|
+
self.websocket_server.broadcast_agent_event(
|
|
284
|
+
event=event_type,
|
|
285
|
+
run_id=run_id,
|
|
286
|
+
parent_session_id=data.get("parent_session_id", ""),
|
|
287
|
+
session_id=data.get("session_id"),
|
|
288
|
+
mode=data.get("mode"),
|
|
289
|
+
provider=data.get("provider"),
|
|
290
|
+
pid=data.get("pid"),
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
task.add_done_callback(_log_broadcast_exception)
|
|
294
|
+
|
|
295
|
+
registry.add_event_callback(broadcast_agent_event)
|
|
296
|
+
logger.debug("Agent event broadcasting enabled")
|
|
297
|
+
|
|
298
|
+
async def _metrics_cleanup_loop(self) -> None:
|
|
299
|
+
"""Background loop for periodic metrics cleanup (every 24 hours)."""
|
|
300
|
+
interval_seconds = 24 * 60 * 60 # 24 hours
|
|
301
|
+
|
|
302
|
+
while not self._shutdown_requested:
|
|
303
|
+
try:
|
|
304
|
+
await asyncio.sleep(interval_seconds)
|
|
305
|
+
deleted = self.metrics_manager.cleanup_old_metrics()
|
|
306
|
+
if deleted > 0:
|
|
307
|
+
logger.info(f"Periodic metrics cleanup: removed {deleted} old entries")
|
|
308
|
+
except asyncio.CancelledError:
|
|
309
|
+
break
|
|
310
|
+
except Exception as e:
|
|
311
|
+
logger.error(f"Error in metrics cleanup loop: {e}")
|
|
312
|
+
|
|
313
|
+
def _check_memory_v2_migration(self) -> None:
|
|
314
|
+
"""Check if Memory V2 migration is needed and log a suggestion.
|
|
315
|
+
|
|
316
|
+
Checks if there are memories in the database but few/no cross-references,
|
|
317
|
+
suggesting the user run `gobby memory migrate-v2`.
|
|
318
|
+
"""
|
|
319
|
+
if not self.memory_manager:
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
# Get memory count
|
|
324
|
+
memories = self.memory_manager.list_memories(limit=1)
|
|
325
|
+
if not memories:
|
|
326
|
+
# No memories, nothing to migrate
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
# Get total memory count for the threshold
|
|
330
|
+
all_memories = self.memory_manager.list_memories(limit=10000)
|
|
331
|
+
memory_count = len(all_memories)
|
|
332
|
+
|
|
333
|
+
if memory_count < 5:
|
|
334
|
+
# Too few memories to warrant migration check
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
# Get crossref count
|
|
338
|
+
from gobby.storage.memories import LocalMemoryManager
|
|
339
|
+
|
|
340
|
+
storage = LocalMemoryManager(self.database)
|
|
341
|
+
crossrefs = storage.get_all_crossrefs(limit=1)
|
|
342
|
+
|
|
343
|
+
if not crossrefs and memory_count >= 5:
|
|
344
|
+
logger.warning(
|
|
345
|
+
f"Memory V2 migration recommended: {memory_count} memories found "
|
|
346
|
+
"but no cross-references. Run 'gobby memory migrate-v2' to enable "
|
|
347
|
+
"semantic search and automatic memory linking."
|
|
348
|
+
)
|
|
349
|
+
except Exception as e:
|
|
350
|
+
# Don't fail startup on migration check errors
|
|
351
|
+
logger.debug(f"Memory migration check failed (non-fatal): {e}")
|
|
352
|
+
|
|
353
|
+
def _setup_signal_handlers(self) -> None:
|
|
354
|
+
loop = asyncio.get_running_loop()
|
|
355
|
+
|
|
356
|
+
def handle_shutdown() -> None:
|
|
357
|
+
logger.info("Received shutdown signal, initiating graceful shutdown...")
|
|
358
|
+
self._shutdown_requested = True
|
|
359
|
+
|
|
360
|
+
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
361
|
+
loop.add_signal_handler(sig, handle_shutdown)
|
|
362
|
+
|
|
363
|
+
async def run(self) -> None:
|
|
364
|
+
try:
|
|
365
|
+
self._setup_signal_handlers()
|
|
366
|
+
|
|
367
|
+
# Connect MCP servers
|
|
368
|
+
try:
|
|
369
|
+
await asyncio.wait_for(self.mcp_proxy.connect_all(), timeout=10.0)
|
|
370
|
+
except TimeoutError:
|
|
371
|
+
logger.warning("MCP connection timed out")
|
|
372
|
+
except Exception as e:
|
|
373
|
+
logger.error(f"MCP connection failed: {e}")
|
|
374
|
+
|
|
375
|
+
# Run metrics cleanup on startup
|
|
376
|
+
try:
|
|
377
|
+
deleted = self.metrics_manager.cleanup_old_metrics()
|
|
378
|
+
if deleted > 0:
|
|
379
|
+
logger.info(f"Startup metrics cleanup: removed {deleted} old entries")
|
|
380
|
+
except Exception as e:
|
|
381
|
+
logger.warning(f"Metrics cleanup failed: {e}")
|
|
382
|
+
|
|
383
|
+
# Check for pending Memory V2 migration
|
|
384
|
+
self._check_memory_v2_migration()
|
|
385
|
+
|
|
386
|
+
# Start Message Processor
|
|
387
|
+
if self.message_processor:
|
|
388
|
+
await self.message_processor.start()
|
|
389
|
+
|
|
390
|
+
# Start Session Lifecycle Manager
|
|
391
|
+
await self.lifecycle_manager.start()
|
|
392
|
+
|
|
393
|
+
# Start periodic metrics cleanup (every 24 hours)
|
|
394
|
+
self._metrics_cleanup_task = asyncio.create_task(
|
|
395
|
+
self._metrics_cleanup_loop(),
|
|
396
|
+
name="metrics-cleanup",
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# Start WebSocket server
|
|
400
|
+
websocket_task = None
|
|
401
|
+
if self.websocket_server:
|
|
402
|
+
websocket_task = asyncio.create_task(self.websocket_server.start())
|
|
403
|
+
|
|
404
|
+
# Start HTTP server
|
|
405
|
+
# nosec B104: 0.0.0.0 binding is intentional - daemon serves local network
|
|
406
|
+
config = uvicorn.Config(
|
|
407
|
+
self.http_server.app,
|
|
408
|
+
host="0.0.0.0", # nosec B104 - local daemon needs network access
|
|
409
|
+
port=self.http_server.port,
|
|
410
|
+
log_level="warning",
|
|
411
|
+
access_log=False,
|
|
412
|
+
)
|
|
413
|
+
server = uvicorn.Server(config)
|
|
414
|
+
server_task = asyncio.create_task(server.serve())
|
|
415
|
+
|
|
416
|
+
# Wait for shutdown
|
|
417
|
+
while not self._shutdown_requested:
|
|
418
|
+
await asyncio.sleep(0.5)
|
|
419
|
+
|
|
420
|
+
# Cleanup with timeouts to prevent hanging
|
|
421
|
+
server.should_exit = True
|
|
422
|
+
try:
|
|
423
|
+
await asyncio.wait_for(server_task, timeout=3.0)
|
|
424
|
+
except TimeoutError:
|
|
425
|
+
logger.warning("HTTP server shutdown timed out")
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
await asyncio.wait_for(self.lifecycle_manager.stop(), timeout=2.0)
|
|
429
|
+
except TimeoutError:
|
|
430
|
+
logger.warning("Lifecycle manager shutdown timed out")
|
|
431
|
+
|
|
432
|
+
if self.message_processor:
|
|
433
|
+
try:
|
|
434
|
+
await asyncio.wait_for(self.message_processor.stop(), timeout=2.0)
|
|
435
|
+
except TimeoutError:
|
|
436
|
+
logger.warning("Message processor shutdown timed out")
|
|
437
|
+
|
|
438
|
+
if websocket_task:
|
|
439
|
+
websocket_task.cancel()
|
|
440
|
+
try:
|
|
441
|
+
await asyncio.wait_for(websocket_task, timeout=3.0)
|
|
442
|
+
except (asyncio.CancelledError, TimeoutError):
|
|
443
|
+
logger.warning("WebSocket server shutdown timed out or cancelled")
|
|
444
|
+
|
|
445
|
+
# Cancel metrics cleanup task
|
|
446
|
+
if self._metrics_cleanup_task and not self._metrics_cleanup_task.done():
|
|
447
|
+
self._metrics_cleanup_task.cancel()
|
|
448
|
+
try:
|
|
449
|
+
await asyncio.wait_for(self._metrics_cleanup_task, timeout=2.0)
|
|
450
|
+
except (asyncio.CancelledError, TimeoutError):
|
|
451
|
+
pass
|
|
452
|
+
|
|
453
|
+
try:
|
|
454
|
+
await asyncio.wait_for(self.mcp_proxy.disconnect_all(), timeout=3.0)
|
|
455
|
+
except TimeoutError:
|
|
456
|
+
logger.warning("MCP disconnect timed out")
|
|
457
|
+
|
|
458
|
+
logger.info("Shutdown complete")
|
|
459
|
+
|
|
460
|
+
except Exception as e:
|
|
461
|
+
logger.error(f"Fatal error: {e}", exc_info=True)
|
|
462
|
+
sys.exit(1)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
async def run_gobby(config_path: Path | None = None, verbose: bool = False) -> None:
|
|
466
|
+
runner = GobbyRunner(config_path=config_path, verbose=verbose)
|
|
467
|
+
await runner.run()
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def main(config_path: Path | None = None, verbose: bool = False) -> None:
|
|
471
|
+
try:
|
|
472
|
+
asyncio.run(run_gobby(config_path=config_path, verbose=verbose))
|
|
473
|
+
except KeyboardInterrupt:
|
|
474
|
+
sys.exit(0)
|
|
475
|
+
except Exception as e:
|
|
476
|
+
logger.error(f"Fatal error: {e}", exc_info=True)
|
|
477
|
+
sys.exit(1)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
if __name__ == "__main__":
|
|
481
|
+
import argparse
|
|
482
|
+
|
|
483
|
+
parser = argparse.ArgumentParser(description="Run Gobby daemon")
|
|
484
|
+
parser.add_argument("--verbose", action="store_true", help="Enable verbose logging")
|
|
485
|
+
parser.add_argument("--config", type=Path, help="Path to config file")
|
|
486
|
+
|
|
487
|
+
args = parser.parse_args()
|
|
488
|
+
main(config_path=args.config, verbose=args.verbose)
|
gobby/search/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared search backend abstraction.
|
|
3
|
+
|
|
4
|
+
Provides pluggable search backends for semantic search:
|
|
5
|
+
- TF-IDF (default) - Built-in local search using scikit-learn (sklearn)
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from gobby.search import SearchBackend, get_search_backend, TFIDFSearcher
|
|
9
|
+
|
|
10
|
+
backend = get_search_backend("tfidf")
|
|
11
|
+
backend.fit([(id, content) for id, content in items])
|
|
12
|
+
results = backend.search("query text", top_k=10)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from gobby.search.protocol import SearchBackend, SearchResult, get_search_backend
|
|
16
|
+
from gobby.search.tfidf import TFIDFSearcher
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"SearchBackend",
|
|
20
|
+
"SearchResult",
|
|
21
|
+
"TFIDFSearcher",
|
|
22
|
+
"get_search_backend",
|
|
23
|
+
]
|
gobby/search/protocol.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Search backend protocol definition.
|
|
2
|
+
|
|
3
|
+
Defines the interface that all search backends must implement.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, Protocol, runtime_checkable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@runtime_checkable
|
|
12
|
+
class SearchBackend(Protocol):
|
|
13
|
+
"""
|
|
14
|
+
Protocol for pluggable search backends.
|
|
15
|
+
|
|
16
|
+
Backends must implement:
|
|
17
|
+
- fit(): Build/rebuild the search index from item contents
|
|
18
|
+
- search(): Find relevant items for a query
|
|
19
|
+
- needs_refit(): Check if index needs rebuilding
|
|
20
|
+
|
|
21
|
+
The protocol uses structural typing, so any class with these methods
|
|
22
|
+
will satisfy the protocol without inheritance.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def fit(self, items: list[tuple[str, str]]) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Build or rebuild the search index.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
items: List of (item_id, content) tuples to index
|
|
31
|
+
|
|
32
|
+
This should be called:
|
|
33
|
+
- On startup to build initial index
|
|
34
|
+
- After bulk item operations
|
|
35
|
+
- When needs_refit() returns True
|
|
36
|
+
"""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
def search(self, query: str, top_k: int = 10) -> list[tuple[str, float]]:
|
|
40
|
+
"""
|
|
41
|
+
Search for items matching the query.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
query: Search query text
|
|
45
|
+
top_k: Maximum number of results to return
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
List of (item_id, similarity_score) tuples, sorted by
|
|
49
|
+
relevance (highest similarity first). Similarity scores
|
|
50
|
+
are typically in range [0, 1] but may vary by backend.
|
|
51
|
+
"""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
def needs_refit(self) -> bool:
|
|
55
|
+
"""
|
|
56
|
+
Check if the search index needs rebuilding.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
True if fit() should be called before search()
|
|
60
|
+
"""
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SearchResult:
|
|
65
|
+
"""Result from a search query with item ID and similarity score."""
|
|
66
|
+
|
|
67
|
+
__slots__ = ("item_id", "similarity")
|
|
68
|
+
|
|
69
|
+
def __init__(self, item_id: str, similarity: float):
|
|
70
|
+
self.item_id = item_id
|
|
71
|
+
self.similarity = similarity
|
|
72
|
+
|
|
73
|
+
def __repr__(self) -> str:
|
|
74
|
+
return f"SearchResult(item_id={self.item_id!r}, similarity={self.similarity:.4f})"
|
|
75
|
+
|
|
76
|
+
def to_tuple(self) -> tuple[str, float]:
|
|
77
|
+
"""Convert to (item_id, similarity) tuple for backwards compatibility."""
|
|
78
|
+
return (self.item_id, self.similarity)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_search_backend(backend_type: str, **kwargs: Any) -> SearchBackend:
|
|
82
|
+
"""
|
|
83
|
+
Factory function for search backends.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
backend_type: Type of backend - currently only "tfidf" is supported
|
|
87
|
+
**kwargs: Backend-specific configuration
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
SearchBackend instance
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
ValueError: If backend_type is not "tfidf"
|
|
94
|
+
ImportError: If required dependencies are not installed
|
|
95
|
+
"""
|
|
96
|
+
from typing import cast
|
|
97
|
+
|
|
98
|
+
if backend_type == "tfidf":
|
|
99
|
+
from gobby.search.tfidf import TFIDFSearcher
|
|
100
|
+
|
|
101
|
+
return cast(SearchBackend, TFIDFSearcher(**kwargs))
|
|
102
|
+
|
|
103
|
+
else:
|
|
104
|
+
raise ValueError(f"Unknown search backend: {backend_type}. Valid options: tfidf")
|