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,339 @@
1
+ """
2
+ Gemini implementation of AgentExecutor.
3
+
4
+ Supports two authentication modes:
5
+ - api_key: Use GEMINI_API_KEY environment variable or provided key
6
+ - adc: Use Google Application Default Credentials (gcloud auth)
7
+ """
8
+
9
+ import asyncio
10
+ import logging
11
+ import os
12
+ from typing import Any, Literal
13
+
14
+ from gobby.llm.executor import (
15
+ AgentExecutor,
16
+ AgentResult,
17
+ ToolCallRecord,
18
+ ToolHandler,
19
+ ToolResult,
20
+ ToolSchema,
21
+ )
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Auth mode type
26
+ GeminiAuthMode = Literal["api_key", "adc"]
27
+
28
+
29
+ class GeminiExecutor(AgentExecutor):
30
+ """
31
+ Gemini implementation of AgentExecutor.
32
+
33
+ Supports two authentication modes:
34
+ - api_key: Uses GEMINI_API_KEY environment variable or provided key
35
+ - adc: Uses Google Application Default Credentials (run `gcloud auth application-default login`)
36
+
37
+ The executor implements a proper agentic loop:
38
+ 1. Send prompt to Gemini with function declarations
39
+ 2. When Gemini requests a function call, call tool_handler
40
+ 3. Send function result back to Gemini
41
+ 4. Repeat until Gemini stops requesting functions or limits are reached
42
+
43
+ Example:
44
+ >>> executor = GeminiExecutor(auth_mode="api_key", api_key="...")
45
+ >>> result = await executor.run(
46
+ ... prompt="Create a task",
47
+ ... tools=[ToolSchema(name="create_task", ...)],
48
+ ... tool_handler=my_handler,
49
+ ... )
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ auth_mode: GeminiAuthMode = "api_key",
55
+ api_key: str | None = None,
56
+ default_model: str = "gemini-2.0-flash",
57
+ ):
58
+ """
59
+ Initialize GeminiExecutor.
60
+
61
+ Args:
62
+ auth_mode: Authentication mode ("api_key" or "adc").
63
+ api_key: Gemini API key (optional for api_key mode, uses GEMINI_API_KEY env var).
64
+ default_model: Default model to use if not specified in run().
65
+ """
66
+ self.auth_mode = auth_mode
67
+ self.default_model = default_model
68
+ self.logger = logger
69
+ self._genai: Any = None
70
+
71
+ try:
72
+ import google.generativeai as genai
73
+
74
+ if auth_mode == "adc":
75
+ # Use Application Default Credentials
76
+ try:
77
+ import google.auth
78
+
79
+ credentials, _project = google.auth.default()
80
+ genai.configure(credentials=credentials)
81
+ self._genai = genai
82
+ self.logger.debug("Gemini initialized with ADC credentials")
83
+ except Exception as e:
84
+ raise ValueError(
85
+ f"Failed to initialize Gemini with ADC: {e}. "
86
+ "Run 'gcloud auth application-default login' to authenticate."
87
+ ) from e
88
+ else:
89
+ # Use API key from parameter or environment
90
+ key = api_key or os.environ.get("GEMINI_API_KEY")
91
+ if not key:
92
+ raise ValueError(
93
+ "API key required for api_key mode. "
94
+ "Provide api_key parameter or set GEMINI_API_KEY env var."
95
+ )
96
+ genai.configure(api_key=key)
97
+ self._genai = genai
98
+ self.logger.debug("Gemini initialized with API key")
99
+
100
+ except ImportError as e:
101
+ raise ImportError(
102
+ "google-generativeai package not found. "
103
+ "Please install with `pip install google-generativeai`."
104
+ ) from e
105
+
106
+ @property
107
+ def provider_name(self) -> str:
108
+ """Return the provider name."""
109
+ return "gemini"
110
+
111
+ def _convert_tools_to_gemini_format(self, tools: list[ToolSchema]) -> list[dict[str, Any]]:
112
+ """Convert ToolSchema list to Gemini function declarations format."""
113
+ function_declarations = []
114
+ for tool in tools:
115
+ # Build parameter schema
116
+ params = tool.input_schema.copy()
117
+ # Ensure type is object
118
+ if "type" not in params:
119
+ params["type"] = "object"
120
+
121
+ function_declarations.append(
122
+ {
123
+ "name": tool.name,
124
+ "description": tool.description,
125
+ "parameters": params,
126
+ }
127
+ )
128
+ return function_declarations
129
+
130
+ async def run(
131
+ self,
132
+ prompt: str,
133
+ tools: list[ToolSchema],
134
+ tool_handler: ToolHandler,
135
+ system_prompt: str | None = None,
136
+ model: str | None = None,
137
+ max_turns: int = 10,
138
+ timeout: float = 120.0,
139
+ ) -> AgentResult:
140
+ """
141
+ Execute an agentic loop with function calling.
142
+
143
+ Runs Gemini with the given prompt, calling tools via tool_handler
144
+ until completion, max_turns, or timeout.
145
+
146
+ Args:
147
+ prompt: The user prompt to process.
148
+ tools: List of available tools with their schemas.
149
+ tool_handler: Callback to execute tool calls.
150
+ system_prompt: Optional system prompt.
151
+ model: Optional model override.
152
+ max_turns: Maximum turns before stopping (default: 10).
153
+ timeout: Maximum execution time in seconds (default: 120.0).
154
+
155
+ Returns:
156
+ AgentResult with output, status, and tool call records.
157
+ """
158
+ if self._genai is None:
159
+ return AgentResult(
160
+ output="",
161
+ status="error",
162
+ error="Gemini client not initialized",
163
+ turns_used=0,
164
+ )
165
+
166
+ tool_calls: list[ToolCallRecord] = []
167
+ effective_model = model or self.default_model
168
+
169
+ # Track turns in outer scope so timeout handler can access the count
170
+ turns_counter = [0]
171
+
172
+ async def _run_loop() -> AgentResult:
173
+ turns_used = 0
174
+ final_output = ""
175
+ genai = self._genai
176
+ if genai is None:
177
+ raise RuntimeError("GeminiExecutor genai not initialized")
178
+
179
+ # Create the model with tools
180
+ gemini_tools = self._convert_tools_to_gemini_format(tools)
181
+
182
+ # Create Tool instance (SDK expects Tool objects, not plain dicts)
183
+ tool_instance = None
184
+ if gemini_tools:
185
+ tool_instance = genai.protos.Tool(function_declarations=gemini_tools)
186
+
187
+ # Create model with system instruction
188
+ generation_config = {
189
+ "max_output_tokens": 8192,
190
+ "temperature": 0.7,
191
+ }
192
+
193
+ model_instance = genai.GenerativeModel(
194
+ model_name=effective_model,
195
+ system_instruction=system_prompt or "You are a helpful assistant.",
196
+ generation_config=generation_config,
197
+ tools=[tool_instance] if tool_instance else None,
198
+ )
199
+
200
+ # Start chat
201
+ chat = model_instance.start_chat()
202
+
203
+ # Send initial message
204
+ try:
205
+ response = await chat.send_message_async(prompt)
206
+ except Exception as e:
207
+ self.logger.error(f"Gemini API error: {e}")
208
+ return AgentResult(
209
+ output="",
210
+ status="error",
211
+ tool_calls=tool_calls,
212
+ error=f"Gemini API error: {e}",
213
+ turns_used=0,
214
+ )
215
+
216
+ while turns_used < max_turns:
217
+ turns_used += 1
218
+ turns_counter[0] = turns_used
219
+
220
+ # Extract function calls and text from response
221
+ function_calls: list[dict[str, Any]] = []
222
+
223
+ for candidate in response.candidates:
224
+ for part in candidate.content.parts:
225
+ # Check for text content
226
+ if hasattr(part, "text") and part.text:
227
+ final_output = part.text
228
+
229
+ # Check for function call
230
+ if hasattr(part, "function_call") and part.function_call:
231
+ fc = part.function_call
232
+ function_calls.append(
233
+ {
234
+ "name": fc.name,
235
+ "args": dict(fc.args) if fc.args else {},
236
+ }
237
+ )
238
+
239
+ # If no function calls, we're done
240
+ if not function_calls:
241
+ return AgentResult(
242
+ output=final_output,
243
+ status="success",
244
+ tool_calls=tool_calls,
245
+ turns_used=turns_used,
246
+ )
247
+
248
+ # Handle function calls
249
+ function_responses = []
250
+
251
+ for fc in function_calls:
252
+ tool_name = fc["name"]
253
+ arguments = fc["args"]
254
+
255
+ # Record the tool call
256
+ record = ToolCallRecord(
257
+ tool_name=tool_name,
258
+ arguments=arguments,
259
+ )
260
+ tool_calls.append(record)
261
+
262
+ # Execute via handler
263
+ try:
264
+ result = await tool_handler(tool_name, arguments)
265
+ record.result = result
266
+
267
+ # Format result for Gemini
268
+ if result.success:
269
+ # Use 'is not None' to preserve legitimate falsy values like 0, False, {}
270
+ response_data = (
271
+ result.result
272
+ if result.result is not None
273
+ else {"status": "success"}
274
+ )
275
+ else:
276
+ response_data = {"error": result.error}
277
+
278
+ function_responses.append(
279
+ genai.protos.Part(
280
+ function_response=genai.protos.FunctionResponse(
281
+ name=tool_name,
282
+ response=(
283
+ response_data
284
+ if isinstance(response_data, dict)
285
+ else {"result": response_data}
286
+ ),
287
+ )
288
+ )
289
+ )
290
+ except Exception as e:
291
+ self.logger.error(f"Tool handler error for {tool_name}: {e}")
292
+ record.result = ToolResult(
293
+ tool_name=tool_name,
294
+ success=False,
295
+ error=str(e),
296
+ )
297
+ function_responses.append(
298
+ genai.protos.Part(
299
+ function_response=genai.protos.FunctionResponse(
300
+ name=tool_name,
301
+ response={"error": str(e)},
302
+ )
303
+ )
304
+ )
305
+
306
+ # Send function responses back to Gemini
307
+ try:
308
+ response = await chat.send_message_async(function_responses)
309
+ # Response will be processed in the next iteration of the while loop
310
+ # which extracts function calls and text directly from the response object
311
+ except Exception as e:
312
+ self.logger.error(f"Error sending function response: {e}")
313
+ return AgentResult(
314
+ output=final_output,
315
+ status="error",
316
+ tool_calls=tool_calls,
317
+ error=f"Error sending function response: {e}",
318
+ turns_used=turns_used,
319
+ )
320
+
321
+ # Max turns reached
322
+ return AgentResult(
323
+ output=final_output,
324
+ status="partial",
325
+ tool_calls=tool_calls,
326
+ turns_used=turns_used,
327
+ )
328
+
329
+ # Run with timeout
330
+ try:
331
+ return await asyncio.wait_for(_run_loop(), timeout=timeout)
332
+ except TimeoutError:
333
+ return AgentResult(
334
+ output="",
335
+ status="timeout",
336
+ tool_calls=tool_calls,
337
+ error=f"Execution timed out after {timeout}s",
338
+ turns_used=turns_counter[0],
339
+ )
gobby/llm/litellm.py ADDED
@@ -0,0 +1,287 @@
1
+ """
2
+ LiteLLM implementation of LLMProvider.
3
+
4
+ LiteLLM provides a unified interface to many LLM providers (OpenAI, Anthropic,
5
+ Mistral, Cohere, etc.) through their APIs using BYOK (Bring Your Own Key).
6
+
7
+ This provider is useful when users want to use their own API keys for
8
+ multiple different providers without needing separate provider implementations.
9
+ """
10
+
11
+ import json
12
+ import logging
13
+ from typing import Any
14
+
15
+ from gobby.config.app import DaemonConfig
16
+ from gobby.llm.base import AuthMode, LLMProvider
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class LiteLLMProvider(LLMProvider):
22
+ """
23
+ LiteLLM implementation of LLMProvider.
24
+
25
+ Uses API key-based authentication (BYOK) for multiple providers.
26
+ API keys are read from:
27
+ 1. llm_providers.api_keys in config (e.g., OPENAI_API_KEY, MISTRAL_API_KEY)
28
+ 2. Environment variables as fallback
29
+
30
+ Example config:
31
+ llm_providers:
32
+ litellm:
33
+ models: gpt-4o-mini,mistral-large
34
+ auth_mode: api_key
35
+ api_keys:
36
+ OPENAI_API_KEY: sk-...
37
+ MISTRAL_API_KEY: ...
38
+ """
39
+
40
+ @property
41
+ def provider_name(self) -> str:
42
+ """Return provider name."""
43
+ return "litellm"
44
+
45
+ @property
46
+ def auth_mode(self) -> AuthMode:
47
+ """LiteLLM uses API key authentication."""
48
+ return "api_key"
49
+
50
+ def __init__(self, config: DaemonConfig):
51
+ """
52
+ Initialize LiteLLMProvider.
53
+
54
+ Args:
55
+ config: Client configuration with optional api_keys in llm_providers.
56
+ """
57
+ self.config = config
58
+ self.logger = logger
59
+ self._litellm = None
60
+ self._api_keys: dict[str, str] = {}
61
+
62
+ # Load API keys from config
63
+ if config.llm_providers and config.llm_providers.api_keys:
64
+ self._api_keys = config.llm_providers.api_keys.copy()
65
+
66
+ try:
67
+ import litellm
68
+
69
+ self._litellm = litellm
70
+
71
+ # Set API keys in litellm's environment
72
+ # LiteLLM reads from os.environ, so we set them there
73
+ import os
74
+
75
+ for key, value in self._api_keys.items():
76
+ if value and key not in os.environ:
77
+ os.environ[key] = value
78
+ self.logger.debug(f"Set {key} from config")
79
+
80
+ self.logger.debug("LiteLLM provider initialized")
81
+
82
+ except ImportError:
83
+ self.logger.error(
84
+ "litellm package not found. Please install with `pip install litellm`."
85
+ )
86
+ except Exception as e:
87
+ self.logger.error(f"Failed to initialize LiteLLM: {e}")
88
+
89
+ def _get_model(self, task: str) -> str:
90
+ """
91
+ Get the model to use for a specific task.
92
+
93
+ Args:
94
+ task: Task type ("summary" or "title")
95
+
96
+ Returns:
97
+ Model name string
98
+ """
99
+ if task == "summary":
100
+ if self.config.session_summary:
101
+ return self.config.session_summary.model or "gpt-4o-mini"
102
+ return "gpt-4o-mini"
103
+ elif task == "title":
104
+ if self.config.title_synthesis:
105
+ return self.config.title_synthesis.model or "gpt-4o-mini"
106
+ return "gpt-4o-mini"
107
+ else:
108
+ return "gpt-4o-mini"
109
+
110
+ async def generate_summary(
111
+ self, context: dict[str, Any], prompt_template: str | None = None
112
+ ) -> str:
113
+ """
114
+ Generate session summary using LiteLLM.
115
+ """
116
+ if not self._litellm:
117
+ return "Session summary unavailable (LiteLLM not initialized)"
118
+
119
+ # Build formatted context for prompt template
120
+ formatted_context = {
121
+ "transcript_summary": context.get("transcript_summary", ""),
122
+ "last_messages": json.dumps(context.get("last_messages", []), indent=2),
123
+ "git_status": context.get("git_status", ""),
124
+ "file_changes": context.get("file_changes", ""),
125
+ **{
126
+ k: v
127
+ for k, v in context.items()
128
+ if k not in ["transcript_summary", "last_messages", "git_status", "file_changes"]
129
+ },
130
+ }
131
+
132
+ # Build prompt - prompt_template is required
133
+ if not prompt_template:
134
+ raise ValueError(
135
+ "prompt_template is required for generate_summary. "
136
+ "Configure 'session_summary.prompt' in ~/.gobby/config.yaml"
137
+ )
138
+ prompt = prompt_template.format(**formatted_context)
139
+
140
+ try:
141
+ # Use LiteLLM's async completion
142
+ response = await self._litellm.acompletion(
143
+ model=self._get_model("summary"),
144
+ messages=[
145
+ {
146
+ "role": "system",
147
+ "content": "You are a session summary generator. Create comprehensive, actionable summaries.",
148
+ },
149
+ {"role": "user", "content": prompt},
150
+ ],
151
+ max_tokens=4000,
152
+ )
153
+ return response.choices[0].message.content or ""
154
+ except Exception as e:
155
+ self.logger.error(f"Failed to generate summary with LiteLLM: {e}")
156
+ return f"Session summary generation failed: {e}"
157
+
158
+ async def synthesize_title(
159
+ self, user_prompt: str, prompt_template: str | None = None
160
+ ) -> str | None:
161
+ """
162
+ Synthesize session title using LiteLLM.
163
+ """
164
+ if not self._litellm:
165
+ return None
166
+
167
+ # Build prompt - prompt_template is required
168
+ if not prompt_template:
169
+ raise ValueError(
170
+ "prompt_template is required for synthesize_title. "
171
+ "Configure 'title_synthesis.prompt' in ~/.gobby/config.yaml"
172
+ )
173
+ prompt = prompt_template.format(user_prompt=user_prompt)
174
+
175
+ try:
176
+ response = await self._litellm.acompletion(
177
+ model=self._get_model("title"),
178
+ messages=[
179
+ {
180
+ "role": "system",
181
+ "content": "You are a session title generator. Create concise, descriptive titles.",
182
+ },
183
+ {"role": "user", "content": prompt},
184
+ ],
185
+ max_tokens=50,
186
+ )
187
+ return (response.choices[0].message.content or "").strip()
188
+ except Exception as e:
189
+ self.logger.error(f"Failed to synthesize title with LiteLLM: {e}")
190
+ return None
191
+
192
+ async def generate_text(
193
+ self,
194
+ prompt: str,
195
+ system_prompt: str | None = None,
196
+ model: str | None = None,
197
+ ) -> str:
198
+ """
199
+ Generate text using LiteLLM.
200
+ """
201
+ if not self._litellm:
202
+ return "Generation unavailable (LiteLLM not initialized)"
203
+
204
+ try:
205
+ response = await self._litellm.acompletion(
206
+ model=model or "gpt-4o-mini",
207
+ messages=[
208
+ {
209
+ "role": "system",
210
+ "content": system_prompt or "You are a helpful assistant.",
211
+ },
212
+ {"role": "user", "content": prompt},
213
+ ],
214
+ max_tokens=4000,
215
+ )
216
+ return response.choices[0].message.content or ""
217
+ except Exception as e:
218
+ self.logger.error(f"Failed to generate text with LiteLLM: {e}")
219
+ return f"Generation failed: {e}"
220
+
221
+ async def describe_image(
222
+ self,
223
+ image_path: str,
224
+ context: str | None = None,
225
+ ) -> str:
226
+ """
227
+ Generate a text description of an image using LiteLLM's vision support.
228
+
229
+ Args:
230
+ image_path: Path to the image file to describe
231
+ context: Optional context to guide the description
232
+
233
+ Returns:
234
+ Text description of the image
235
+ """
236
+ import base64
237
+ import mimetypes
238
+ from pathlib import Path
239
+
240
+ if not self._litellm:
241
+ return "Image description unavailable (LiteLLM not initialized)"
242
+
243
+ # Validate image exists
244
+ path = Path(image_path)
245
+ if not path.exists():
246
+ return f"Image not found: {image_path}"
247
+
248
+ # Read and encode image
249
+ try:
250
+ image_data = path.read_bytes()
251
+ image_base64 = base64.standard_b64encode(image_data).decode("utf-8")
252
+ except Exception as e:
253
+ self.logger.error(f"Failed to read image {image_path}: {e}")
254
+ return f"Failed to read image: {e}"
255
+
256
+ # Determine media type
257
+ mime_type, _ = mimetypes.guess_type(str(path))
258
+ if mime_type not in ["image/jpeg", "image/png", "image/gif", "image/webp"]:
259
+ mime_type = "image/png"
260
+
261
+ # Build prompt
262
+ prompt = "Please describe this image in detail, focusing on the key visual elements and any text visible."
263
+ if context:
264
+ prompt = f"{context}\n\n{prompt}"
265
+
266
+ try:
267
+ # Use LiteLLM's vision support (works with gpt-4o, claude-3, etc.)
268
+ response = await self._litellm.acompletion(
269
+ model="gpt-4o-mini", # Default to a vision-capable model
270
+ messages=[
271
+ {
272
+ "role": "user",
273
+ "content": [
274
+ {"type": "text", "text": prompt},
275
+ {
276
+ "type": "image_url",
277
+ "image_url": {"url": f"data:{mime_type};base64,{image_base64}"},
278
+ },
279
+ ],
280
+ }
281
+ ],
282
+ max_tokens=1000,
283
+ )
284
+ return response.choices[0].message.content or ""
285
+ except Exception as e:
286
+ self.logger.error(f"Failed to describe image with LiteLLM: {e}")
287
+ return f"Image description failed: {e}"