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
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Tool fallback resolution service.
|
|
2
|
+
|
|
3
|
+
Provides alternative tool suggestions when a tool call fails,
|
|
4
|
+
using semantic similarity and success rate weighting.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from gobby.mcp_proxy.metrics import ToolMetricsManager
|
|
15
|
+
from gobby.mcp_proxy.semantic_search import SemanticToolSearch
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class FallbackSuggestion:
|
|
22
|
+
"""A suggested alternative tool."""
|
|
23
|
+
|
|
24
|
+
server_name: str
|
|
25
|
+
tool_name: str
|
|
26
|
+
description: str | None
|
|
27
|
+
similarity: float
|
|
28
|
+
success_rate: float | None
|
|
29
|
+
score: float # Combined ranking score
|
|
30
|
+
|
|
31
|
+
def to_dict(self) -> dict[str, Any]:
|
|
32
|
+
"""Convert to dictionary."""
|
|
33
|
+
return {
|
|
34
|
+
"server_name": self.server_name,
|
|
35
|
+
"tool_name": self.tool_name,
|
|
36
|
+
"description": self.description,
|
|
37
|
+
"similarity": round(self.similarity, 4),
|
|
38
|
+
"success_rate": round(self.success_rate, 4) if self.success_rate else None,
|
|
39
|
+
"score": round(self.score, 4),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ToolFallbackResolver:
|
|
44
|
+
"""
|
|
45
|
+
Resolves alternative tools when a tool call fails.
|
|
46
|
+
|
|
47
|
+
Uses semantic similarity search to find similar tools and
|
|
48
|
+
weights results by historical success rate from metrics.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
# Default weight for similarity vs success_rate in scoring
|
|
52
|
+
DEFAULT_SIMILARITY_WEIGHT = 0.7
|
|
53
|
+
DEFAULT_SUCCESS_WEIGHT = 0.3
|
|
54
|
+
|
|
55
|
+
# Minimum similarity threshold for candidates
|
|
56
|
+
DEFAULT_MIN_SIMILARITY = 0.3
|
|
57
|
+
|
|
58
|
+
# Default success rate when no metrics available
|
|
59
|
+
DEFAULT_SUCCESS_RATE = 0.5
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
semantic_search: SemanticToolSearch,
|
|
64
|
+
metrics_manager: ToolMetricsManager | None = None,
|
|
65
|
+
similarity_weight: float = DEFAULT_SIMILARITY_WEIGHT,
|
|
66
|
+
success_weight: float = DEFAULT_SUCCESS_WEIGHT,
|
|
67
|
+
min_similarity: float = DEFAULT_MIN_SIMILARITY,
|
|
68
|
+
):
|
|
69
|
+
"""
|
|
70
|
+
Initialize the fallback resolver.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
semantic_search: SemanticToolSearch instance for finding similar tools
|
|
74
|
+
metrics_manager: Optional ToolMetricsManager for success rate data
|
|
75
|
+
similarity_weight: Weight for similarity score (0-1)
|
|
76
|
+
success_weight: Weight for success rate score (0-1)
|
|
77
|
+
min_similarity: Minimum similarity threshold for candidates
|
|
78
|
+
"""
|
|
79
|
+
self._semantic_search = semantic_search
|
|
80
|
+
self._metrics_manager = metrics_manager
|
|
81
|
+
self._similarity_weight = similarity_weight
|
|
82
|
+
self._success_weight = success_weight
|
|
83
|
+
self._min_similarity = min_similarity
|
|
84
|
+
|
|
85
|
+
async def find_alternatives(
|
|
86
|
+
self,
|
|
87
|
+
failed_tool_name: str,
|
|
88
|
+
failed_tool_description: str | None = None,
|
|
89
|
+
error_context: str | None = None,
|
|
90
|
+
server_name: str | None = None,
|
|
91
|
+
project_id: str | None = None,
|
|
92
|
+
top_k: int = 5,
|
|
93
|
+
exclude_failed: bool = True,
|
|
94
|
+
) -> list[FallbackSuggestion]:
|
|
95
|
+
"""
|
|
96
|
+
Find alternative tools similar to a failed tool.
|
|
97
|
+
|
|
98
|
+
Uses semantic search to find tools with similar descriptions,
|
|
99
|
+
then weights by historical success rate.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
failed_tool_name: Name of the tool that failed
|
|
103
|
+
failed_tool_description: Description of the failed tool (if available)
|
|
104
|
+
error_context: Error message or context for better matching
|
|
105
|
+
server_name: Server the failed tool belongs to (for exclusion)
|
|
106
|
+
project_id: Project ID for scoping the search
|
|
107
|
+
top_k: Maximum number of suggestions to return
|
|
108
|
+
exclude_failed: Whether to exclude the failed tool from results
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
List of FallbackSuggestion sorted by combined score (descending)
|
|
112
|
+
"""
|
|
113
|
+
if not project_id:
|
|
114
|
+
logger.warning("No project_id provided for fallback search")
|
|
115
|
+
return []
|
|
116
|
+
|
|
117
|
+
# Build query from tool info and error context
|
|
118
|
+
query = self._build_search_query(failed_tool_name, failed_tool_description, error_context)
|
|
119
|
+
|
|
120
|
+
# Get semantically similar tools
|
|
121
|
+
try:
|
|
122
|
+
search_results = await self._semantic_search.search_tools(
|
|
123
|
+
query=query,
|
|
124
|
+
project_id=project_id,
|
|
125
|
+
top_k=top_k * 2, # Get extra for filtering
|
|
126
|
+
min_similarity=self._min_similarity,
|
|
127
|
+
)
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.error(f"Semantic search failed in fallback resolver: {e}")
|
|
130
|
+
return []
|
|
131
|
+
|
|
132
|
+
if not search_results:
|
|
133
|
+
logger.debug(f"No semantic matches found for '{failed_tool_name}'")
|
|
134
|
+
return []
|
|
135
|
+
|
|
136
|
+
# Filter out the failed tool if requested
|
|
137
|
+
if exclude_failed:
|
|
138
|
+
search_results = [
|
|
139
|
+
r
|
|
140
|
+
for r in search_results
|
|
141
|
+
if not (r.tool_name == failed_tool_name and r.server_name == server_name)
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
# Enrich with success rates and compute combined scores
|
|
145
|
+
suggestions = []
|
|
146
|
+
for result in search_results[:top_k]:
|
|
147
|
+
success_rate = self._get_success_rate(result.server_name, result.tool_name, project_id)
|
|
148
|
+
|
|
149
|
+
score = self._compute_score(result.similarity, success_rate)
|
|
150
|
+
|
|
151
|
+
suggestions.append(
|
|
152
|
+
FallbackSuggestion(
|
|
153
|
+
server_name=result.server_name,
|
|
154
|
+
tool_name=result.tool_name,
|
|
155
|
+
description=result.description,
|
|
156
|
+
similarity=result.similarity,
|
|
157
|
+
success_rate=success_rate,
|
|
158
|
+
score=score,
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Sort by combined score (descending)
|
|
163
|
+
suggestions.sort(key=lambda s: s.score, reverse=True)
|
|
164
|
+
|
|
165
|
+
logger.debug(f"Found {len(suggestions)} fallback suggestions for '{failed_tool_name}'")
|
|
166
|
+
return suggestions
|
|
167
|
+
|
|
168
|
+
def _build_search_query(
|
|
169
|
+
self,
|
|
170
|
+
tool_name: str,
|
|
171
|
+
description: str | None,
|
|
172
|
+
error_context: str | None,
|
|
173
|
+
) -> str:
|
|
174
|
+
"""
|
|
175
|
+
Build a search query from tool info and error context.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
tool_name: Name of the failed tool
|
|
179
|
+
description: Tool description
|
|
180
|
+
error_context: Error message or context
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Search query string
|
|
184
|
+
"""
|
|
185
|
+
parts = [f"Tool similar to: {tool_name}"]
|
|
186
|
+
|
|
187
|
+
if description:
|
|
188
|
+
parts.append(f"Description: {description}")
|
|
189
|
+
|
|
190
|
+
if error_context:
|
|
191
|
+
# Extract key terms from error, avoiding noise
|
|
192
|
+
parts.append(f"Context: {error_context[:200]}")
|
|
193
|
+
|
|
194
|
+
return "\n".join(parts)
|
|
195
|
+
|
|
196
|
+
def _get_success_rate(self, server_name: str, tool_name: str, project_id: str) -> float | None:
|
|
197
|
+
"""
|
|
198
|
+
Get success rate for a tool from metrics.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
server_name: Server name
|
|
202
|
+
tool_name: Tool name
|
|
203
|
+
project_id: Project ID
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Success rate (0-1) or None if no metrics available
|
|
207
|
+
"""
|
|
208
|
+
if not self._metrics_manager:
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
return self._metrics_manager.get_tool_success_rate(
|
|
213
|
+
server_name=server_name,
|
|
214
|
+
tool_name=tool_name,
|
|
215
|
+
project_id=project_id,
|
|
216
|
+
)
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.debug(f"Failed to get success rate for {server_name}/{tool_name}: {e}")
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
def _compute_score(self, similarity: float, success_rate: float | None) -> float:
|
|
222
|
+
"""
|
|
223
|
+
Compute combined ranking score.
|
|
224
|
+
|
|
225
|
+
Score = (similarity * similarity_weight) + (success_rate * success_weight)
|
|
226
|
+
|
|
227
|
+
When success_rate is None, uses default value to avoid penalizing
|
|
228
|
+
tools without metrics history.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
similarity: Cosine similarity score (0-1)
|
|
232
|
+
success_rate: Historical success rate (0-1) or None
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Combined score (0-1)
|
|
236
|
+
"""
|
|
237
|
+
effective_success_rate = (
|
|
238
|
+
success_rate if success_rate is not None else self.DEFAULT_SUCCESS_RATE
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return similarity * self._similarity_weight + effective_success_rate * self._success_weight
|
|
242
|
+
|
|
243
|
+
async def find_alternatives_for_error(
|
|
244
|
+
self,
|
|
245
|
+
server_name: str,
|
|
246
|
+
tool_name: str,
|
|
247
|
+
error_message: str,
|
|
248
|
+
project_id: str,
|
|
249
|
+
top_k: int = 3,
|
|
250
|
+
) -> list[dict[str, Any]]:
|
|
251
|
+
"""
|
|
252
|
+
Convenience method for call_tool integration.
|
|
253
|
+
|
|
254
|
+
Takes error details and returns serialized suggestions.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
server_name: Server where the tool failed
|
|
258
|
+
tool_name: Name of the failed tool
|
|
259
|
+
error_message: Error message from the failure
|
|
260
|
+
project_id: Project ID
|
|
261
|
+
top_k: Number of suggestions to return
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
List of suggestion dictionaries ready for JSON response
|
|
265
|
+
"""
|
|
266
|
+
# Try to get tool description from cached tools
|
|
267
|
+
description = await self._get_tool_description(server_name, tool_name)
|
|
268
|
+
|
|
269
|
+
suggestions = await self.find_alternatives(
|
|
270
|
+
failed_tool_name=tool_name,
|
|
271
|
+
failed_tool_description=description,
|
|
272
|
+
error_context=error_message,
|
|
273
|
+
server_name=server_name,
|
|
274
|
+
project_id=project_id,
|
|
275
|
+
top_k=top_k,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return [s.to_dict() for s in suggestions]
|
|
279
|
+
|
|
280
|
+
async def _get_tool_description(self, server_name: str, tool_name: str) -> str | None:
|
|
281
|
+
"""
|
|
282
|
+
Get tool description from semantic search's cached data.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
server_name: Server name
|
|
286
|
+
tool_name: Tool name
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Tool description or None
|
|
290
|
+
"""
|
|
291
|
+
# The tool info is in the database, accessed via _get_tool_info_map
|
|
292
|
+
# But we don't have project_id here, so we search all
|
|
293
|
+
try:
|
|
294
|
+
row = self._semantic_search.db.fetchone(
|
|
295
|
+
"""
|
|
296
|
+
SELECT t.description
|
|
297
|
+
FROM tools t
|
|
298
|
+
JOIN mcp_servers s ON t.mcp_server_id = s.id
|
|
299
|
+
WHERE s.name = ? AND t.name = ?
|
|
300
|
+
LIMIT 1
|
|
301
|
+
""",
|
|
302
|
+
(server_name, tool_name),
|
|
303
|
+
)
|
|
304
|
+
return row["description"] if row else None
|
|
305
|
+
except Exception:
|
|
306
|
+
return None
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Recommendation service."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from gobby.config.app import RecommendToolsConfig
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("gobby.mcp.server")
|
|
13
|
+
|
|
14
|
+
# Search mode type
|
|
15
|
+
SearchMode = Literal["llm", "semantic", "hybrid"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RecommendationService:
|
|
19
|
+
"""Service for recommending tools."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
llm_service: Any,
|
|
24
|
+
mcp_manager: Any,
|
|
25
|
+
semantic_search: Any | None = None,
|
|
26
|
+
project_id: str | None = None,
|
|
27
|
+
config: RecommendToolsConfig | None = None,
|
|
28
|
+
):
|
|
29
|
+
self._llm_service = llm_service
|
|
30
|
+
self._mcp_manager = mcp_manager
|
|
31
|
+
self._semantic_search = semantic_search
|
|
32
|
+
self._project_id = project_id
|
|
33
|
+
self._config = config
|
|
34
|
+
|
|
35
|
+
def _get_config(self) -> RecommendToolsConfig:
|
|
36
|
+
"""Get config with fallback to defaults."""
|
|
37
|
+
if self._config is not None:
|
|
38
|
+
return self._config
|
|
39
|
+
from gobby.config.app import RecommendToolsConfig
|
|
40
|
+
|
|
41
|
+
return RecommendToolsConfig()
|
|
42
|
+
|
|
43
|
+
async def recommend_tools(
|
|
44
|
+
self,
|
|
45
|
+
task_description: str,
|
|
46
|
+
agent_id: str | None = None,
|
|
47
|
+
search_mode: SearchMode = "llm",
|
|
48
|
+
top_k: int = 10,
|
|
49
|
+
min_similarity: float = 0.3,
|
|
50
|
+
project_id: str | None = None,
|
|
51
|
+
) -> dict[str, Any]:
|
|
52
|
+
"""
|
|
53
|
+
Recommend tools based on task description.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
task_description: Description of what the user wants to do
|
|
57
|
+
agent_id: Optional agent ID for filtering (reserved for future use)
|
|
58
|
+
search_mode: How to search for tools:
|
|
59
|
+
- "llm": Use LLM to recommend (default, original behavior)
|
|
60
|
+
- "semantic": Use embedding similarity search
|
|
61
|
+
- "hybrid": Combine semantic search with LLM ranking
|
|
62
|
+
top_k: Maximum recommendations to return (for semantic/hybrid)
|
|
63
|
+
min_similarity: Minimum similarity threshold (for semantic/hybrid)
|
|
64
|
+
project_id: Project ID for semantic/hybrid search (overrides instance default)
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Dict with recommendations and metadata
|
|
68
|
+
"""
|
|
69
|
+
# Use provided project_id or fall back to instance default
|
|
70
|
+
effective_project_id = project_id or self._project_id
|
|
71
|
+
|
|
72
|
+
if search_mode == "semantic":
|
|
73
|
+
return await self._recommend_semantic(
|
|
74
|
+
task_description, top_k, min_similarity, effective_project_id
|
|
75
|
+
)
|
|
76
|
+
elif search_mode == "hybrid":
|
|
77
|
+
return await self._recommend_hybrid(
|
|
78
|
+
task_description, top_k, min_similarity, effective_project_id
|
|
79
|
+
)
|
|
80
|
+
else:
|
|
81
|
+
return await self._recommend_llm(task_description)
|
|
82
|
+
|
|
83
|
+
async def _recommend_semantic(
|
|
84
|
+
self, task_description: str, top_k: int, min_similarity: float, project_id: str | None
|
|
85
|
+
) -> dict[str, Any]:
|
|
86
|
+
"""Recommend tools using semantic similarity search."""
|
|
87
|
+
if not self._semantic_search:
|
|
88
|
+
return {
|
|
89
|
+
"success": False,
|
|
90
|
+
"error": "Semantic search not configured",
|
|
91
|
+
"task": task_description,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if not project_id:
|
|
95
|
+
return {
|
|
96
|
+
"success": False,
|
|
97
|
+
"error": "Project ID not set for semantic search",
|
|
98
|
+
"task": task_description,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
results = await self._semantic_search.search_tools(
|
|
103
|
+
query=task_description,
|
|
104
|
+
project_id=project_id,
|
|
105
|
+
top_k=top_k,
|
|
106
|
+
min_similarity=min_similarity,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
recommendations = [
|
|
110
|
+
{
|
|
111
|
+
"server": r.server_name,
|
|
112
|
+
"tool": r.tool_name,
|
|
113
|
+
"reason": r.description or "Semantically similar to query",
|
|
114
|
+
"similarity": round(r.similarity, 4),
|
|
115
|
+
}
|
|
116
|
+
for r in results
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
"success": True,
|
|
121
|
+
"task": task_description,
|
|
122
|
+
"search_mode": "semantic",
|
|
123
|
+
"recommendation": recommendations,
|
|
124
|
+
"recommendations": recommendations,
|
|
125
|
+
"total_results": len(results),
|
|
126
|
+
}
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error(f"Semantic search failed: {e}")
|
|
129
|
+
return {"success": False, "error": str(e), "task": task_description}
|
|
130
|
+
|
|
131
|
+
async def _recommend_hybrid(
|
|
132
|
+
self, task_description: str, top_k: int, min_similarity: float, project_id: str | None
|
|
133
|
+
) -> dict[str, Any]:
|
|
134
|
+
"""Recommend tools using semantic search + LLM re-ranking."""
|
|
135
|
+
# First get semantic results
|
|
136
|
+
semantic_result = await self._recommend_semantic(
|
|
137
|
+
task_description,
|
|
138
|
+
top_k * 2,
|
|
139
|
+
min_similarity,
|
|
140
|
+
project_id, # Get more for re-ranking
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if not semantic_result.get("success") or not semantic_result.get("recommendations"):
|
|
144
|
+
# Fall back to pure LLM if semantic fails
|
|
145
|
+
return await self._recommend_llm(task_description)
|
|
146
|
+
|
|
147
|
+
# Use LLM to re-rank and add reasoning
|
|
148
|
+
try:
|
|
149
|
+
config = self._get_config()
|
|
150
|
+
candidates = semantic_result["recommendations"]
|
|
151
|
+
candidate_list = "\n".join(
|
|
152
|
+
f"- {c['server']}/{c['tool']}: {c.get('reason', 'No description')}"
|
|
153
|
+
for c in candidates
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
prompt = config.hybrid_rerank_prompt.format(
|
|
157
|
+
task_description=task_description,
|
|
158
|
+
candidate_list=candidate_list,
|
|
159
|
+
top_k=top_k,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
provider = self._llm_service.get_default_provider()
|
|
163
|
+
response = await provider.generate_text(prompt)
|
|
164
|
+
|
|
165
|
+
# Parse LLM response
|
|
166
|
+
if "```json" in response:
|
|
167
|
+
response = response.split("```json")[1].split("```")[0].strip()
|
|
168
|
+
elif "```" in response:
|
|
169
|
+
response = response.split("```")[1].split("```")[0].strip()
|
|
170
|
+
|
|
171
|
+
data = json.loads(response)
|
|
172
|
+
recommendations = data.get("recommendations", [])[:top_k]
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
"success": True,
|
|
176
|
+
"task": task_description,
|
|
177
|
+
"search_mode": "hybrid",
|
|
178
|
+
"recommendation": recommendations,
|
|
179
|
+
"recommendations": recommendations,
|
|
180
|
+
"semantic_candidates": len(candidates),
|
|
181
|
+
}
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.warning(f"Hybrid LLM re-ranking failed, using semantic results: {e}")
|
|
184
|
+
# Fall back to semantic results
|
|
185
|
+
semantic_result["search_mode"] = "hybrid_fallback"
|
|
186
|
+
return semantic_result
|
|
187
|
+
|
|
188
|
+
async def _recommend_llm(self, task_description: str) -> dict[str, Any]:
|
|
189
|
+
"""Recommend tools using LLM (original behavior)."""
|
|
190
|
+
try:
|
|
191
|
+
config = self._get_config()
|
|
192
|
+
available_servers = self._mcp_manager.get_available_servers()
|
|
193
|
+
|
|
194
|
+
prompt = config.llm_prompt.format(
|
|
195
|
+
task_description=task_description,
|
|
196
|
+
available_servers=", ".join(available_servers),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
provider = self._llm_service.get_default_provider()
|
|
200
|
+
response = await provider.generate_text(prompt)
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
if "```json" in response:
|
|
204
|
+
response = response.split("```json")[1].split("```")[0].strip()
|
|
205
|
+
elif "```" in response:
|
|
206
|
+
response = response.split("```")[1].split("```")[0].strip()
|
|
207
|
+
|
|
208
|
+
data = json.loads(response)
|
|
209
|
+
recommendations = data.get("recommendations", [])
|
|
210
|
+
except (json.JSONDecodeError, KeyError, IndexError) as e:
|
|
211
|
+
recommendations = []
|
|
212
|
+
logger.warning(f"Failed to parse LLM recommendation response: {e}")
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
"success": True,
|
|
216
|
+
"task": task_description,
|
|
217
|
+
"search_mode": "llm",
|
|
218
|
+
"recommendation": recommendations,
|
|
219
|
+
"recommendations": recommendations,
|
|
220
|
+
"available_servers": available_servers,
|
|
221
|
+
}
|
|
222
|
+
except Exception as e:
|
|
223
|
+
logger.error(f"Error generating recommendations: {e}")
|
|
224
|
+
return {"success": False, "error": str(e), "task": task_description}
|