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,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}"
|