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.
Files changed (383) hide show
  1. gobby/__init__.py +3 -0
  2. gobby/adapters/__init__.py +30 -0
  3. gobby/adapters/base.py +93 -0
  4. gobby/adapters/claude_code.py +276 -0
  5. gobby/adapters/codex.py +1292 -0
  6. gobby/adapters/gemini.py +343 -0
  7. gobby/agents/__init__.py +37 -0
  8. gobby/agents/codex_session.py +120 -0
  9. gobby/agents/constants.py +112 -0
  10. gobby/agents/context.py +362 -0
  11. gobby/agents/definitions.py +133 -0
  12. gobby/agents/gemini_session.py +111 -0
  13. gobby/agents/registry.py +618 -0
  14. gobby/agents/runner.py +968 -0
  15. gobby/agents/session.py +259 -0
  16. gobby/agents/spawn.py +916 -0
  17. gobby/agents/spawners/__init__.py +77 -0
  18. gobby/agents/spawners/base.py +142 -0
  19. gobby/agents/spawners/cross_platform.py +266 -0
  20. gobby/agents/spawners/embedded.py +225 -0
  21. gobby/agents/spawners/headless.py +226 -0
  22. gobby/agents/spawners/linux.py +125 -0
  23. gobby/agents/spawners/macos.py +277 -0
  24. gobby/agents/spawners/windows.py +308 -0
  25. gobby/agents/tty_config.py +319 -0
  26. gobby/autonomous/__init__.py +32 -0
  27. gobby/autonomous/progress_tracker.py +447 -0
  28. gobby/autonomous/stop_registry.py +269 -0
  29. gobby/autonomous/stuck_detector.py +383 -0
  30. gobby/cli/__init__.py +67 -0
  31. gobby/cli/__main__.py +8 -0
  32. gobby/cli/agents.py +529 -0
  33. gobby/cli/artifacts.py +266 -0
  34. gobby/cli/daemon.py +329 -0
  35. gobby/cli/extensions.py +526 -0
  36. gobby/cli/github.py +263 -0
  37. gobby/cli/init.py +53 -0
  38. gobby/cli/install.py +614 -0
  39. gobby/cli/installers/__init__.py +37 -0
  40. gobby/cli/installers/antigravity.py +65 -0
  41. gobby/cli/installers/claude.py +363 -0
  42. gobby/cli/installers/codex.py +192 -0
  43. gobby/cli/installers/gemini.py +294 -0
  44. gobby/cli/installers/git_hooks.py +377 -0
  45. gobby/cli/installers/shared.py +737 -0
  46. gobby/cli/linear.py +250 -0
  47. gobby/cli/mcp.py +30 -0
  48. gobby/cli/mcp_proxy.py +698 -0
  49. gobby/cli/memory.py +304 -0
  50. gobby/cli/merge.py +384 -0
  51. gobby/cli/projects.py +79 -0
  52. gobby/cli/sessions.py +622 -0
  53. gobby/cli/tasks/__init__.py +30 -0
  54. gobby/cli/tasks/_utils.py +658 -0
  55. gobby/cli/tasks/ai.py +1025 -0
  56. gobby/cli/tasks/commits.py +169 -0
  57. gobby/cli/tasks/crud.py +685 -0
  58. gobby/cli/tasks/deps.py +135 -0
  59. gobby/cli/tasks/labels.py +63 -0
  60. gobby/cli/tasks/main.py +273 -0
  61. gobby/cli/tasks/search.py +178 -0
  62. gobby/cli/tui.py +34 -0
  63. gobby/cli/utils.py +513 -0
  64. gobby/cli/workflows.py +927 -0
  65. gobby/cli/worktrees.py +481 -0
  66. gobby/config/__init__.py +129 -0
  67. gobby/config/app.py +551 -0
  68. gobby/config/extensions.py +167 -0
  69. gobby/config/features.py +472 -0
  70. gobby/config/llm_providers.py +98 -0
  71. gobby/config/logging.py +66 -0
  72. gobby/config/mcp.py +346 -0
  73. gobby/config/persistence.py +247 -0
  74. gobby/config/servers.py +141 -0
  75. gobby/config/sessions.py +250 -0
  76. gobby/config/tasks.py +784 -0
  77. gobby/hooks/__init__.py +104 -0
  78. gobby/hooks/artifact_capture.py +213 -0
  79. gobby/hooks/broadcaster.py +243 -0
  80. gobby/hooks/event_handlers.py +723 -0
  81. gobby/hooks/events.py +218 -0
  82. gobby/hooks/git.py +169 -0
  83. gobby/hooks/health_monitor.py +171 -0
  84. gobby/hooks/hook_manager.py +856 -0
  85. gobby/hooks/hook_types.py +575 -0
  86. gobby/hooks/plugins.py +813 -0
  87. gobby/hooks/session_coordinator.py +396 -0
  88. gobby/hooks/verification_runner.py +268 -0
  89. gobby/hooks/webhooks.py +339 -0
  90. gobby/install/claude/commands/gobby/bug.md +51 -0
  91. gobby/install/claude/commands/gobby/chore.md +51 -0
  92. gobby/install/claude/commands/gobby/epic.md +52 -0
  93. gobby/install/claude/commands/gobby/eval.md +235 -0
  94. gobby/install/claude/commands/gobby/feat.md +49 -0
  95. gobby/install/claude/commands/gobby/nit.md +52 -0
  96. gobby/install/claude/commands/gobby/ref.md +52 -0
  97. gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
  98. gobby/install/claude/hooks/hook_dispatcher.py +364 -0
  99. gobby/install/claude/hooks/validate_settings.py +102 -0
  100. gobby/install/claude/hooks-template.json +118 -0
  101. gobby/install/codex/hooks/hook_dispatcher.py +153 -0
  102. gobby/install/codex/prompts/forget.md +7 -0
  103. gobby/install/codex/prompts/memories.md +7 -0
  104. gobby/install/codex/prompts/recall.md +7 -0
  105. gobby/install/codex/prompts/remember.md +13 -0
  106. gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
  107. gobby/install/gemini/hooks-template.json +138 -0
  108. gobby/install/shared/plugins/code_guardian.py +456 -0
  109. gobby/install/shared/plugins/example_notify.py +331 -0
  110. gobby/integrations/__init__.py +10 -0
  111. gobby/integrations/github.py +145 -0
  112. gobby/integrations/linear.py +145 -0
  113. gobby/llm/__init__.py +40 -0
  114. gobby/llm/base.py +120 -0
  115. gobby/llm/claude.py +578 -0
  116. gobby/llm/claude_executor.py +503 -0
  117. gobby/llm/codex.py +322 -0
  118. gobby/llm/codex_executor.py +513 -0
  119. gobby/llm/executor.py +316 -0
  120. gobby/llm/factory.py +34 -0
  121. gobby/llm/gemini.py +258 -0
  122. gobby/llm/gemini_executor.py +339 -0
  123. gobby/llm/litellm.py +287 -0
  124. gobby/llm/litellm_executor.py +303 -0
  125. gobby/llm/resolver.py +499 -0
  126. gobby/llm/service.py +236 -0
  127. gobby/mcp_proxy/__init__.py +29 -0
  128. gobby/mcp_proxy/actions.py +175 -0
  129. gobby/mcp_proxy/daemon_control.py +198 -0
  130. gobby/mcp_proxy/importer.py +436 -0
  131. gobby/mcp_proxy/lazy.py +325 -0
  132. gobby/mcp_proxy/manager.py +798 -0
  133. gobby/mcp_proxy/metrics.py +609 -0
  134. gobby/mcp_proxy/models.py +139 -0
  135. gobby/mcp_proxy/registries.py +215 -0
  136. gobby/mcp_proxy/schema_hash.py +381 -0
  137. gobby/mcp_proxy/semantic_search.py +706 -0
  138. gobby/mcp_proxy/server.py +549 -0
  139. gobby/mcp_proxy/services/__init__.py +0 -0
  140. gobby/mcp_proxy/services/fallback.py +306 -0
  141. gobby/mcp_proxy/services/recommendation.py +224 -0
  142. gobby/mcp_proxy/services/server_mgmt.py +214 -0
  143. gobby/mcp_proxy/services/system.py +72 -0
  144. gobby/mcp_proxy/services/tool_filter.py +231 -0
  145. gobby/mcp_proxy/services/tool_proxy.py +309 -0
  146. gobby/mcp_proxy/stdio.py +565 -0
  147. gobby/mcp_proxy/tools/__init__.py +27 -0
  148. gobby/mcp_proxy/tools/agents.py +1103 -0
  149. gobby/mcp_proxy/tools/artifacts.py +207 -0
  150. gobby/mcp_proxy/tools/hub.py +335 -0
  151. gobby/mcp_proxy/tools/internal.py +337 -0
  152. gobby/mcp_proxy/tools/memory.py +543 -0
  153. gobby/mcp_proxy/tools/merge.py +422 -0
  154. gobby/mcp_proxy/tools/metrics.py +283 -0
  155. gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
  156. gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
  157. gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
  158. gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
  159. gobby/mcp_proxy/tools/orchestration/review.py +736 -0
  160. gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
  161. gobby/mcp_proxy/tools/session_messages.py +1056 -0
  162. gobby/mcp_proxy/tools/task_dependencies.py +219 -0
  163. gobby/mcp_proxy/tools/task_expansion.py +591 -0
  164. gobby/mcp_proxy/tools/task_github.py +393 -0
  165. gobby/mcp_proxy/tools/task_linear.py +379 -0
  166. gobby/mcp_proxy/tools/task_orchestration.py +77 -0
  167. gobby/mcp_proxy/tools/task_readiness.py +522 -0
  168. gobby/mcp_proxy/tools/task_sync.py +351 -0
  169. gobby/mcp_proxy/tools/task_validation.py +843 -0
  170. gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
  171. gobby/mcp_proxy/tools/tasks/_context.py +112 -0
  172. gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
  173. gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
  174. gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
  175. gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
  176. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
  177. gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
  178. gobby/mcp_proxy/tools/tasks/_search.py +215 -0
  179. gobby/mcp_proxy/tools/tasks/_session.py +125 -0
  180. gobby/mcp_proxy/tools/workflows.py +973 -0
  181. gobby/mcp_proxy/tools/worktrees.py +1264 -0
  182. gobby/mcp_proxy/transports/__init__.py +0 -0
  183. gobby/mcp_proxy/transports/base.py +95 -0
  184. gobby/mcp_proxy/transports/factory.py +44 -0
  185. gobby/mcp_proxy/transports/http.py +139 -0
  186. gobby/mcp_proxy/transports/stdio.py +213 -0
  187. gobby/mcp_proxy/transports/websocket.py +136 -0
  188. gobby/memory/backends/__init__.py +116 -0
  189. gobby/memory/backends/mem0.py +408 -0
  190. gobby/memory/backends/memu.py +485 -0
  191. gobby/memory/backends/null.py +111 -0
  192. gobby/memory/backends/openmemory.py +537 -0
  193. gobby/memory/backends/sqlite.py +304 -0
  194. gobby/memory/context.py +87 -0
  195. gobby/memory/manager.py +1001 -0
  196. gobby/memory/protocol.py +451 -0
  197. gobby/memory/search/__init__.py +66 -0
  198. gobby/memory/search/text.py +127 -0
  199. gobby/memory/viz.py +258 -0
  200. gobby/prompts/__init__.py +13 -0
  201. gobby/prompts/defaults/expansion/system.md +119 -0
  202. gobby/prompts/defaults/expansion/user.md +48 -0
  203. gobby/prompts/defaults/external_validation/agent.md +72 -0
  204. gobby/prompts/defaults/external_validation/external.md +63 -0
  205. gobby/prompts/defaults/external_validation/spawn.md +83 -0
  206. gobby/prompts/defaults/external_validation/system.md +6 -0
  207. gobby/prompts/defaults/features/import_mcp.md +22 -0
  208. gobby/prompts/defaults/features/import_mcp_github.md +17 -0
  209. gobby/prompts/defaults/features/import_mcp_search.md +16 -0
  210. gobby/prompts/defaults/features/recommend_tools.md +32 -0
  211. gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
  212. gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
  213. gobby/prompts/defaults/features/server_description.md +20 -0
  214. gobby/prompts/defaults/features/server_description_system.md +6 -0
  215. gobby/prompts/defaults/features/task_description.md +31 -0
  216. gobby/prompts/defaults/features/task_description_system.md +6 -0
  217. gobby/prompts/defaults/features/tool_summary.md +17 -0
  218. gobby/prompts/defaults/features/tool_summary_system.md +6 -0
  219. gobby/prompts/defaults/research/step.md +58 -0
  220. gobby/prompts/defaults/validation/criteria.md +47 -0
  221. gobby/prompts/defaults/validation/validate.md +38 -0
  222. gobby/prompts/loader.py +346 -0
  223. gobby/prompts/models.py +113 -0
  224. gobby/py.typed +0 -0
  225. gobby/runner.py +488 -0
  226. gobby/search/__init__.py +23 -0
  227. gobby/search/protocol.py +104 -0
  228. gobby/search/tfidf.py +232 -0
  229. gobby/servers/__init__.py +7 -0
  230. gobby/servers/http.py +636 -0
  231. gobby/servers/models.py +31 -0
  232. gobby/servers/routes/__init__.py +23 -0
  233. gobby/servers/routes/admin.py +416 -0
  234. gobby/servers/routes/dependencies.py +118 -0
  235. gobby/servers/routes/mcp/__init__.py +24 -0
  236. gobby/servers/routes/mcp/hooks.py +135 -0
  237. gobby/servers/routes/mcp/plugins.py +121 -0
  238. gobby/servers/routes/mcp/tools.py +1337 -0
  239. gobby/servers/routes/mcp/webhooks.py +159 -0
  240. gobby/servers/routes/sessions.py +582 -0
  241. gobby/servers/websocket.py +766 -0
  242. gobby/sessions/__init__.py +13 -0
  243. gobby/sessions/analyzer.py +322 -0
  244. gobby/sessions/lifecycle.py +240 -0
  245. gobby/sessions/manager.py +563 -0
  246. gobby/sessions/processor.py +225 -0
  247. gobby/sessions/summary.py +532 -0
  248. gobby/sessions/transcripts/__init__.py +41 -0
  249. gobby/sessions/transcripts/base.py +125 -0
  250. gobby/sessions/transcripts/claude.py +386 -0
  251. gobby/sessions/transcripts/codex.py +143 -0
  252. gobby/sessions/transcripts/gemini.py +195 -0
  253. gobby/storage/__init__.py +21 -0
  254. gobby/storage/agents.py +409 -0
  255. gobby/storage/artifact_classifier.py +341 -0
  256. gobby/storage/artifacts.py +285 -0
  257. gobby/storage/compaction.py +67 -0
  258. gobby/storage/database.py +357 -0
  259. gobby/storage/inter_session_messages.py +194 -0
  260. gobby/storage/mcp.py +680 -0
  261. gobby/storage/memories.py +562 -0
  262. gobby/storage/merge_resolutions.py +550 -0
  263. gobby/storage/migrations.py +860 -0
  264. gobby/storage/migrations_legacy.py +1359 -0
  265. gobby/storage/projects.py +166 -0
  266. gobby/storage/session_messages.py +251 -0
  267. gobby/storage/session_tasks.py +97 -0
  268. gobby/storage/sessions.py +817 -0
  269. gobby/storage/task_dependencies.py +223 -0
  270. gobby/storage/tasks/__init__.py +42 -0
  271. gobby/storage/tasks/_aggregates.py +180 -0
  272. gobby/storage/tasks/_crud.py +449 -0
  273. gobby/storage/tasks/_id.py +104 -0
  274. gobby/storage/tasks/_lifecycle.py +311 -0
  275. gobby/storage/tasks/_manager.py +889 -0
  276. gobby/storage/tasks/_models.py +300 -0
  277. gobby/storage/tasks/_ordering.py +119 -0
  278. gobby/storage/tasks/_path_cache.py +110 -0
  279. gobby/storage/tasks/_queries.py +343 -0
  280. gobby/storage/tasks/_search.py +143 -0
  281. gobby/storage/workflow_audit.py +393 -0
  282. gobby/storage/worktrees.py +547 -0
  283. gobby/sync/__init__.py +29 -0
  284. gobby/sync/github.py +333 -0
  285. gobby/sync/linear.py +304 -0
  286. gobby/sync/memories.py +284 -0
  287. gobby/sync/tasks.py +641 -0
  288. gobby/tasks/__init__.py +8 -0
  289. gobby/tasks/build_verification.py +193 -0
  290. gobby/tasks/commits.py +633 -0
  291. gobby/tasks/context.py +747 -0
  292. gobby/tasks/criteria.py +342 -0
  293. gobby/tasks/enhanced_validator.py +226 -0
  294. gobby/tasks/escalation.py +263 -0
  295. gobby/tasks/expansion.py +626 -0
  296. gobby/tasks/external_validator.py +764 -0
  297. gobby/tasks/issue_extraction.py +171 -0
  298. gobby/tasks/prompts/expand.py +327 -0
  299. gobby/tasks/research.py +421 -0
  300. gobby/tasks/tdd.py +352 -0
  301. gobby/tasks/tree_builder.py +263 -0
  302. gobby/tasks/validation.py +712 -0
  303. gobby/tasks/validation_history.py +357 -0
  304. gobby/tasks/validation_models.py +89 -0
  305. gobby/tools/__init__.py +0 -0
  306. gobby/tools/summarizer.py +170 -0
  307. gobby/tui/__init__.py +5 -0
  308. gobby/tui/api_client.py +281 -0
  309. gobby/tui/app.py +327 -0
  310. gobby/tui/screens/__init__.py +25 -0
  311. gobby/tui/screens/agents.py +333 -0
  312. gobby/tui/screens/chat.py +450 -0
  313. gobby/tui/screens/dashboard.py +377 -0
  314. gobby/tui/screens/memory.py +305 -0
  315. gobby/tui/screens/metrics.py +231 -0
  316. gobby/tui/screens/orchestrator.py +904 -0
  317. gobby/tui/screens/sessions.py +412 -0
  318. gobby/tui/screens/tasks.py +442 -0
  319. gobby/tui/screens/workflows.py +289 -0
  320. gobby/tui/screens/worktrees.py +174 -0
  321. gobby/tui/widgets/__init__.py +21 -0
  322. gobby/tui/widgets/chat.py +210 -0
  323. gobby/tui/widgets/conductor.py +104 -0
  324. gobby/tui/widgets/menu.py +132 -0
  325. gobby/tui/widgets/message_panel.py +160 -0
  326. gobby/tui/widgets/review_gate.py +224 -0
  327. gobby/tui/widgets/task_tree.py +99 -0
  328. gobby/tui/widgets/token_budget.py +166 -0
  329. gobby/tui/ws_client.py +258 -0
  330. gobby/utils/__init__.py +3 -0
  331. gobby/utils/daemon_client.py +235 -0
  332. gobby/utils/git.py +222 -0
  333. gobby/utils/id.py +38 -0
  334. gobby/utils/json_helpers.py +161 -0
  335. gobby/utils/logging.py +376 -0
  336. gobby/utils/machine_id.py +135 -0
  337. gobby/utils/metrics.py +589 -0
  338. gobby/utils/project_context.py +182 -0
  339. gobby/utils/project_init.py +263 -0
  340. gobby/utils/status.py +256 -0
  341. gobby/utils/validation.py +80 -0
  342. gobby/utils/version.py +23 -0
  343. gobby/workflows/__init__.py +4 -0
  344. gobby/workflows/actions.py +1310 -0
  345. gobby/workflows/approval_flow.py +138 -0
  346. gobby/workflows/artifact_actions.py +103 -0
  347. gobby/workflows/audit_helpers.py +110 -0
  348. gobby/workflows/autonomous_actions.py +286 -0
  349. gobby/workflows/context_actions.py +394 -0
  350. gobby/workflows/definitions.py +130 -0
  351. gobby/workflows/detection_helpers.py +208 -0
  352. gobby/workflows/engine.py +485 -0
  353. gobby/workflows/evaluator.py +669 -0
  354. gobby/workflows/git_utils.py +96 -0
  355. gobby/workflows/hooks.py +169 -0
  356. gobby/workflows/lifecycle_evaluator.py +613 -0
  357. gobby/workflows/llm_actions.py +70 -0
  358. gobby/workflows/loader.py +333 -0
  359. gobby/workflows/mcp_actions.py +60 -0
  360. gobby/workflows/memory_actions.py +272 -0
  361. gobby/workflows/premature_stop.py +164 -0
  362. gobby/workflows/session_actions.py +139 -0
  363. gobby/workflows/state_actions.py +123 -0
  364. gobby/workflows/state_manager.py +104 -0
  365. gobby/workflows/stop_signal_actions.py +163 -0
  366. gobby/workflows/summary_actions.py +344 -0
  367. gobby/workflows/task_actions.py +249 -0
  368. gobby/workflows/task_enforcement_actions.py +901 -0
  369. gobby/workflows/templates.py +52 -0
  370. gobby/workflows/todo_actions.py +84 -0
  371. gobby/workflows/webhook.py +223 -0
  372. gobby/workflows/webhook_executor.py +399 -0
  373. gobby/worktrees/__init__.py +5 -0
  374. gobby/worktrees/git.py +690 -0
  375. gobby/worktrees/merge/__init__.py +20 -0
  376. gobby/worktrees/merge/conflict_parser.py +177 -0
  377. gobby/worktrees/merge/resolver.py +485 -0
  378. gobby-0.2.5.dist-info/METADATA +351 -0
  379. gobby-0.2.5.dist-info/RECORD +383 -0
  380. gobby-0.2.5.dist-info/WHEEL +5 -0
  381. gobby-0.2.5.dist-info/entry_points.txt +2 -0
  382. gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
  383. 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}