gobby 0.2.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
gobby/prompts/loader.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prompt template loader with multi-level override support.
|
|
3
|
+
|
|
4
|
+
Implements prompt loading with precedence:
|
|
5
|
+
1. Inline config (deprecated, for backwards compatibility)
|
|
6
|
+
2. Config path (explicit path in config)
|
|
7
|
+
3. Project file (.gobby/prompts/)
|
|
8
|
+
4. Global file (~/.gobby/prompts/)
|
|
9
|
+
5. Bundled default (src/gobby/prompts/defaults/)
|
|
10
|
+
6. Python constant (strangler fig fallback)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import re
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from functools import lru_cache
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import yaml
|
|
21
|
+
|
|
22
|
+
from .models import PromptTemplate
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
# Default location for bundled prompts
|
|
27
|
+
DEFAULTS_DIR = Path(__file__).parent / "defaults"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PromptLoader:
|
|
31
|
+
"""Loads prompt templates from multiple sources with override precedence.
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
loader = PromptLoader(project_dir=Path("."))
|
|
35
|
+
template = loader.load("expansion/system")
|
|
36
|
+
rendered = loader.render("expansion/system", {"tdd_mode": True})
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
project_dir: Path | None = None,
|
|
42
|
+
global_dir: Path | None = None,
|
|
43
|
+
defaults_dir: Path | None = None,
|
|
44
|
+
):
|
|
45
|
+
"""Initialize the prompt loader.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
project_dir: Project root directory (for .gobby/prompts)
|
|
49
|
+
global_dir: Global config directory (defaults to ~/.gobby)
|
|
50
|
+
defaults_dir: Directory for bundled defaults (auto-detected)
|
|
51
|
+
"""
|
|
52
|
+
self.project_dir = project_dir
|
|
53
|
+
self.global_dir = global_dir or Path.home() / ".gobby"
|
|
54
|
+
self.defaults_dir = defaults_dir or DEFAULTS_DIR
|
|
55
|
+
|
|
56
|
+
# Build search paths in priority order
|
|
57
|
+
self._search_paths: list[Path] = []
|
|
58
|
+
if project_dir:
|
|
59
|
+
self._search_paths.append(project_dir / ".gobby" / "prompts")
|
|
60
|
+
self._search_paths.append(self.global_dir / "prompts")
|
|
61
|
+
self._search_paths.append(self.defaults_dir)
|
|
62
|
+
|
|
63
|
+
# Template cache
|
|
64
|
+
self._cache: dict[str, PromptTemplate] = {}
|
|
65
|
+
|
|
66
|
+
# Fallback registry for strangler fig pattern
|
|
67
|
+
self._fallbacks: dict[str, Callable[[], str]] = {}
|
|
68
|
+
|
|
69
|
+
def register_fallback(self, path: str, getter: Callable[[], str]) -> None:
|
|
70
|
+
"""Register a Python constant fallback for a template path.
|
|
71
|
+
|
|
72
|
+
Used for strangler fig pattern - if template file doesn't exist,
|
|
73
|
+
fall back to the original Python constant.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
path: Template path (e.g., "expansion/system")
|
|
77
|
+
getter: Callable that returns the fallback string
|
|
78
|
+
"""
|
|
79
|
+
self._fallbacks[path] = getter
|
|
80
|
+
|
|
81
|
+
def clear_cache(self) -> None:
|
|
82
|
+
"""Clear the template cache."""
|
|
83
|
+
self._cache.clear()
|
|
84
|
+
|
|
85
|
+
def _find_template_file(self, path: str) -> Path | None:
|
|
86
|
+
"""Find a template file in search paths.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
path: Template path (e.g., "expansion/system")
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Path to template file if found, None otherwise
|
|
93
|
+
"""
|
|
94
|
+
# Add .md extension if not present
|
|
95
|
+
if not path.endswith(".md"):
|
|
96
|
+
path = f"{path}.md"
|
|
97
|
+
|
|
98
|
+
for search_dir in self._search_paths:
|
|
99
|
+
template_path = search_dir / path
|
|
100
|
+
if template_path.exists():
|
|
101
|
+
return template_path
|
|
102
|
+
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
def _parse_frontmatter(self, content: str) -> tuple[dict[str, Any], str]:
|
|
106
|
+
"""Parse YAML frontmatter from template content.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
content: Raw file content
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Tuple of (frontmatter dict, body content)
|
|
113
|
+
"""
|
|
114
|
+
# Match YAML frontmatter between --- markers
|
|
115
|
+
frontmatter_pattern = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
|
|
116
|
+
match = frontmatter_pattern.match(content)
|
|
117
|
+
|
|
118
|
+
if match:
|
|
119
|
+
try:
|
|
120
|
+
frontmatter = yaml.safe_load(match.group(1)) or {}
|
|
121
|
+
body = content[match.end() :]
|
|
122
|
+
return frontmatter, body
|
|
123
|
+
except yaml.YAMLError as e:
|
|
124
|
+
logger.warning(f"Failed to parse frontmatter: {e}")
|
|
125
|
+
return {}, content
|
|
126
|
+
|
|
127
|
+
return {}, content
|
|
128
|
+
|
|
129
|
+
def load(self, path: str) -> PromptTemplate:
|
|
130
|
+
"""Load a prompt template by path.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
path: Template path (e.g., "expansion/system")
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
PromptTemplate instance
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
FileNotFoundError: If template not found and no fallback registered
|
|
140
|
+
"""
|
|
141
|
+
# Check cache first
|
|
142
|
+
if path in self._cache:
|
|
143
|
+
return self._cache[path]
|
|
144
|
+
|
|
145
|
+
# Try to find template file
|
|
146
|
+
template_file = self._find_template_file(path)
|
|
147
|
+
|
|
148
|
+
if template_file:
|
|
149
|
+
content = template_file.read_text(encoding="utf-8")
|
|
150
|
+
frontmatter, body = self._parse_frontmatter(content)
|
|
151
|
+
|
|
152
|
+
template = PromptTemplate.from_frontmatter(
|
|
153
|
+
name=path,
|
|
154
|
+
frontmatter=frontmatter,
|
|
155
|
+
content=body.strip(),
|
|
156
|
+
source_path=template_file,
|
|
157
|
+
)
|
|
158
|
+
self._cache[path] = template
|
|
159
|
+
logger.debug(f"Loaded prompt template '{path}' from {template_file}")
|
|
160
|
+
return template
|
|
161
|
+
|
|
162
|
+
# Fall back to registered Python constant
|
|
163
|
+
if path in self._fallbacks:
|
|
164
|
+
fallback_content = self._fallbacks[path]()
|
|
165
|
+
template = PromptTemplate(
|
|
166
|
+
name=path,
|
|
167
|
+
description=f"Fallback for {path}",
|
|
168
|
+
content=fallback_content,
|
|
169
|
+
source_path=None,
|
|
170
|
+
)
|
|
171
|
+
self._cache[path] = template
|
|
172
|
+
logger.debug(f"Using fallback for prompt template '{path}'")
|
|
173
|
+
return template
|
|
174
|
+
|
|
175
|
+
raise FileNotFoundError(f"Prompt template not found: {path}")
|
|
176
|
+
|
|
177
|
+
def render(
|
|
178
|
+
self,
|
|
179
|
+
path: str,
|
|
180
|
+
context: dict[str, Any] | None = None,
|
|
181
|
+
strict: bool = False,
|
|
182
|
+
) -> str:
|
|
183
|
+
"""Load and render a template with context.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
path: Template path
|
|
187
|
+
context: Variables to inject into template
|
|
188
|
+
strict: If True, raise on missing required variables
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Rendered template string
|
|
192
|
+
|
|
193
|
+
Raises:
|
|
194
|
+
FileNotFoundError: If template not found
|
|
195
|
+
ValueError: If strict=True and required variables missing
|
|
196
|
+
"""
|
|
197
|
+
template = self.load(path)
|
|
198
|
+
ctx = template.get_default_context()
|
|
199
|
+
|
|
200
|
+
if context:
|
|
201
|
+
ctx.update(context)
|
|
202
|
+
|
|
203
|
+
# Validate required variables
|
|
204
|
+
if strict:
|
|
205
|
+
errors = template.validate_context(ctx)
|
|
206
|
+
if errors:
|
|
207
|
+
raise ValueError(f"Template validation failed: {'; '.join(errors)}")
|
|
208
|
+
|
|
209
|
+
# Render with Jinja2
|
|
210
|
+
return self._render_jinja(template.content, ctx)
|
|
211
|
+
|
|
212
|
+
def _render_jinja(self, template_str: str, context: dict[str, Any]) -> str:
|
|
213
|
+
"""Render a template string with Jinja2.
|
|
214
|
+
|
|
215
|
+
Uses a safe subset of Jinja2 features.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
template_str: Template content with Jinja2 syntax
|
|
219
|
+
context: Context dict for rendering
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Rendered string
|
|
223
|
+
"""
|
|
224
|
+
try:
|
|
225
|
+
from jinja2 import Environment, StrictUndefined, UndefinedError
|
|
226
|
+
|
|
227
|
+
# Create a restricted Jinja2 environment
|
|
228
|
+
env = Environment( # nosec B701 - generating raw text prompts, not HTML
|
|
229
|
+
autoescape=False,
|
|
230
|
+
undefined=StrictUndefined,
|
|
231
|
+
# Disable dangerous features
|
|
232
|
+
extensions=[],
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Add safe filters
|
|
236
|
+
env.filters["default"] = lambda v, d="": d if v is None else v
|
|
237
|
+
|
|
238
|
+
template = env.from_string(template_str)
|
|
239
|
+
return template.render(**context)
|
|
240
|
+
|
|
241
|
+
except UndefinedError as e:
|
|
242
|
+
logger.warning(f"Template rendering error (undefined variable): {e}")
|
|
243
|
+
# Fall back to simple string formatting for undefined vars
|
|
244
|
+
return self._render_simple(template_str, context)
|
|
245
|
+
except ImportError:
|
|
246
|
+
# Jinja2 not available, use simple formatting
|
|
247
|
+
logger.debug("Jinja2 not available, using simple format")
|
|
248
|
+
return self._render_simple(template_str, context)
|
|
249
|
+
except Exception as e:
|
|
250
|
+
logger.warning(f"Template rendering error: {e}")
|
|
251
|
+
return self._render_simple(template_str, context)
|
|
252
|
+
|
|
253
|
+
def _render_simple(self, template_str: str, context: dict[str, Any]) -> str:
|
|
254
|
+
"""Simple string formatting fallback.
|
|
255
|
+
|
|
256
|
+
Handles {variable} placeholders using str.format().
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
template_str: Template with {var} placeholders
|
|
260
|
+
context: Context dict
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Rendered string
|
|
264
|
+
"""
|
|
265
|
+
try:
|
|
266
|
+
return template_str.format(**context)
|
|
267
|
+
except KeyError:
|
|
268
|
+
# Return as-is if formatting fails
|
|
269
|
+
return template_str
|
|
270
|
+
|
|
271
|
+
def exists(self, path: str) -> bool:
|
|
272
|
+
"""Check if a template exists.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
path: Template path
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
True if template exists (file or fallback)
|
|
279
|
+
"""
|
|
280
|
+
return self._find_template_file(path) is not None or path in self._fallbacks
|
|
281
|
+
|
|
282
|
+
def list_templates(self, category: str | None = None) -> list[str]:
|
|
283
|
+
"""List available template paths.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
category: Optional category to filter (e.g., "expansion")
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
List of template paths
|
|
290
|
+
"""
|
|
291
|
+
templates: set[str] = set()
|
|
292
|
+
|
|
293
|
+
for search_dir in self._search_paths:
|
|
294
|
+
if not search_dir.exists():
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
for md_file in search_dir.rglob("*.md"):
|
|
298
|
+
rel_path = md_file.relative_to(search_dir)
|
|
299
|
+
# Remove .md extension for path
|
|
300
|
+
template_path = str(rel_path.with_suffix(""))
|
|
301
|
+
|
|
302
|
+
if category is None or template_path.startswith(f"{category}/"):
|
|
303
|
+
templates.add(template_path)
|
|
304
|
+
|
|
305
|
+
# Add registered fallbacks
|
|
306
|
+
for fallback_path in self._fallbacks:
|
|
307
|
+
if category is None or fallback_path.startswith(f"{category}/"):
|
|
308
|
+
templates.add(fallback_path)
|
|
309
|
+
|
|
310
|
+
return sorted(templates)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# Module-level cached loader instance
|
|
314
|
+
@lru_cache(maxsize=1)
|
|
315
|
+
def get_default_loader() -> PromptLoader:
|
|
316
|
+
"""Get or create the default prompt loader.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Cached PromptLoader instance
|
|
320
|
+
"""
|
|
321
|
+
return PromptLoader()
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def load_prompt(path: str) -> PromptTemplate:
|
|
325
|
+
"""Convenience function to load a prompt using default loader.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
path: Template path
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
PromptTemplate
|
|
332
|
+
"""
|
|
333
|
+
return get_default_loader().load(path)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def render_prompt(path: str, context: dict[str, Any] | None = None) -> str:
|
|
337
|
+
"""Convenience function to render a prompt using default loader.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
path: Template path
|
|
341
|
+
context: Variables for rendering
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Rendered string
|
|
345
|
+
"""
|
|
346
|
+
return get_default_loader().render(path, context)
|
gobby/prompts/models.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prompt template data models.
|
|
3
|
+
|
|
4
|
+
Contains dataclasses for representing prompt templates with metadata.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class VariableSpec:
|
|
14
|
+
"""Specification for a template variable.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
type: Variable type (str, int, bool, list, dict)
|
|
18
|
+
default: Default value if not provided
|
|
19
|
+
description: Human-readable description
|
|
20
|
+
required: Whether the variable must be provided
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
type: Literal["str", "int", "bool", "list", "dict"] = "str"
|
|
24
|
+
default: Any = None
|
|
25
|
+
description: str = ""
|
|
26
|
+
required: bool = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class PromptTemplate:
|
|
31
|
+
"""A loaded prompt template with metadata.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
name: Template identifier (e.g., "expansion/system")
|
|
35
|
+
description: Human-readable description
|
|
36
|
+
variables: Variable specifications from frontmatter
|
|
37
|
+
content: Raw template content (with Jinja2 syntax)
|
|
38
|
+
source_path: Path the template was loaded from (None for fallbacks)
|
|
39
|
+
version: Template version for compatibility checking
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
name: str
|
|
43
|
+
description: str = ""
|
|
44
|
+
variables: dict[str, VariableSpec] = field(default_factory=dict)
|
|
45
|
+
content: str = ""
|
|
46
|
+
source_path: Path | None = None
|
|
47
|
+
version: str = "1.0"
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_frontmatter(
|
|
51
|
+
cls,
|
|
52
|
+
name: str,
|
|
53
|
+
frontmatter: dict[str, Any],
|
|
54
|
+
content: str,
|
|
55
|
+
source_path: Path | None = None,
|
|
56
|
+
) -> "PromptTemplate":
|
|
57
|
+
"""Create a PromptTemplate from parsed frontmatter.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
name: Template name/path
|
|
61
|
+
frontmatter: Parsed YAML frontmatter
|
|
62
|
+
content: Template content after frontmatter
|
|
63
|
+
source_path: Source file path
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
PromptTemplate instance
|
|
67
|
+
"""
|
|
68
|
+
# Parse variables from frontmatter
|
|
69
|
+
variables: dict[str, VariableSpec] = {}
|
|
70
|
+
if "variables" in frontmatter:
|
|
71
|
+
for var_name, var_spec in frontmatter["variables"].items():
|
|
72
|
+
if isinstance(var_spec, dict):
|
|
73
|
+
variables[var_name] = VariableSpec(
|
|
74
|
+
type=var_spec.get("type", "str"),
|
|
75
|
+
default=var_spec.get("default"),
|
|
76
|
+
description=var_spec.get("description", ""),
|
|
77
|
+
required=var_spec.get("required", False),
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
# Simple form: just a default value
|
|
81
|
+
variables[var_name] = VariableSpec(default=var_spec)
|
|
82
|
+
|
|
83
|
+
return cls(
|
|
84
|
+
name=name,
|
|
85
|
+
description=frontmatter.get("description", ""),
|
|
86
|
+
variables=variables,
|
|
87
|
+
content=content,
|
|
88
|
+
source_path=source_path,
|
|
89
|
+
version=frontmatter.get("version", "1.0"),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def get_default_context(self) -> dict[str, Any]:
|
|
93
|
+
"""Get default values for all variables.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Dict of variable names to their default values
|
|
97
|
+
"""
|
|
98
|
+
return {name: spec.default for name, spec in self.variables.items()}
|
|
99
|
+
|
|
100
|
+
def validate_context(self, context: dict[str, Any]) -> list[str]:
|
|
101
|
+
"""Validate that required variables are provided.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
context: Context dict being passed to render
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
List of error messages (empty if valid)
|
|
108
|
+
"""
|
|
109
|
+
errors: list[str] = []
|
|
110
|
+
for name, spec in self.variables.items():
|
|
111
|
+
if spec.required and name not in context:
|
|
112
|
+
errors.append(f"Required variable '{name}' not provided")
|
|
113
|
+
return errors
|
gobby/py.typed
ADDED
|
File without changes
|