From f54feb380b375eb09f5d00833042579464d0c35a Mon Sep 17 00:00:00 2001 From: Matthieu Monsch Date: Mon, 22 Sep 2025 09:36:56 -0700 Subject: [PATCH 1/5] feat: add bot option flag --- src/git_draft/__main__.py | 9 ++++++++- src/git_draft/bots/__init__.py | 32 ++++++++++++++++++++++++-------- src/git_draft/common.py | 2 +- tests/git_draft/common_test.py | 4 ++-- 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/git_draft/__main__.py b/src/git_draft/__main__.py index b442017..dcfaab9 100644 --- a/src/git_draft/__main__.py +++ b/src/git_draft/__main__.py @@ -110,6 +110,13 @@ def callback( dest="bot", help="AI bot name", ) + parser.add_option( + "-o", + "--bot-option", + action="append", + dest="bot_options", + help="AI bot options", + ) parser.add_option( "-e", "--edit", @@ -193,7 +200,7 @@ async def run() -> None: # noqa: PLR0912 PLR0915 bot_config = bot_configs[0] elif config.bots: bot_config = config.bots[0] - bot = load_bot(bot_config) + bot = load_bot(bot_config, overrides=opts.bot_options) prompt: str | TemplatedPrompt if args: diff --git a/src/git_draft/bots/__init__.py b/src/git_draft/bots/__init__.py index 8154ea2..c15edbc 100644 --- a/src/git_draft/bots/__init__.py +++ b/src/git_draft/bots/__init__.py @@ -1,10 +1,11 @@ """Bot interfaces and built-in implementations""" +from collections.abc import Sequence import importlib import os import sys -from ..common import BotConfig, reindent +from ..common import BotConfig, JSONObject, UnreachableError, reindent from .common import ActionSummary, Bot, Goal, UserFeedback, Worktree @@ -17,10 +18,13 @@ ] -def load_bot(config: BotConfig | None) -> Bot: +def load_bot(config: BotConfig | None, *, overrides: Sequence[str]=()) -> Bot: """Load and return a Bot instance using the provided configuration""" + options = {**config.options} if config and config.options else {} + options.update(_parse_overrides(overrides)) + if not config: - return _default_bot() + return _default_bot(options) if config.pythonpath and config.pythonpath not in sys.path: sys.path.insert(0, config.pythonpath) @@ -35,11 +39,23 @@ def load_bot(config: BotConfig | None) -> Bot: if not factory: raise NotImplementedError(f"Unknown bot factory: {config.factory}") - kwargs = config.kwargs or {} - return factory(**kwargs) + return factory(**options) + + +def _parse_overrides(overrides: str) -> JSONObject: + options = {} + for override in overrides: + match override.split("=", 1): + case [switch]: + options[switch] = True + case [flag, value]: + options[flag] = value + case _: + raise UnreachableError() + return options -def _default_bot() -> Bot: +def _default_bot(options: JSONObject) -> Bot: if not os.environ.get("OPENAI_API_KEY"): raise RuntimeError( reindent( @@ -52,7 +68,7 @@ def _default_bot() -> Bot: ) try: - from .openai_api import new_threads_bot + from .openai_api import new_completions_bot except ImportError: raise RuntimeError( @@ -65,4 +81,4 @@ def _default_bot() -> Bot: ) ) else: - return new_threads_bot() + return new_completions_bot(**options) diff --git a/src/git_draft/common.py b/src/git_draft/common.py index 79a6093..5be0b32 100644 --- a/src/git_draft/common.py +++ b/src/git_draft/common.py @@ -68,7 +68,7 @@ class BotConfig: factory: str name: str | None = None - kwargs: JSONObject | None = None + options: JSONObject | None = None pythonpath: str | None = None diff --git a/tests/git_draft/common_test.py b/tests/git_draft/common_test.py index c000b01..f831b6a 100644 --- a/tests/git_draft/common_test.py +++ b/tests/git_draft/common_test.py @@ -30,7 +30,7 @@ def test_load_ok(self) -> None: [[bots]] name = "bar" factory = "bar" - kwargs = {one=1} + options = {one=1} """ path = sut.Config.folder_path() path.mkdir(parents=True, exist_ok=True) @@ -42,7 +42,7 @@ def test_load_ok(self) -> None: log_level=logging.DEBUG, bots=[ sut.BotConfig(factory="foo:load", pythonpath="./abc"), - sut.BotConfig(factory="bar", name="bar", kwargs={"one": 1}), + sut.BotConfig(factory="bar", name="bar", options={"one": 1}), ], ) From 492fd69fa9cd377f08e3651a6c84f605ff109b13 Mon Sep 17 00:00:00 2001 From: Matthieu Monsch Date: Mon, 22 Sep 2025 09:38:27 -0700 Subject: [PATCH 2/5] fixup! f54feb3 --- src/git_draft/bots/__init__.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/git_draft/bots/__init__.py b/src/git_draft/bots/__init__.py index c15edbc..00595fc 100644 --- a/src/git_draft/bots/__init__.py +++ b/src/git_draft/bots/__init__.py @@ -5,7 +5,13 @@ import os import sys -from ..common import BotConfig, JSONObject, UnreachableError, reindent +from ..common import ( + BotConfig, + JSONObject, + JSONValue, + UnreachableError, + reindent, +) from .common import ActionSummary, Bot, Goal, UserFeedback, Worktree @@ -18,7 +24,9 @@ ] -def load_bot(config: BotConfig | None, *, overrides: Sequence[str]=()) -> Bot: +def load_bot( + config: BotConfig | None, *, overrides: Sequence[str] = () +) -> Bot: """Load and return a Bot instance using the provided configuration""" options = {**config.options} if config and config.options else {} options.update(_parse_overrides(overrides)) @@ -42,8 +50,8 @@ def load_bot(config: BotConfig | None, *, overrides: Sequence[str]=()) -> Bot: return factory(**options) -def _parse_overrides(overrides: str) -> JSONObject: - options = {} +def _parse_overrides(overrides: Sequence[str]) -> JSONObject: + options = dict[str, JSONValue]() for override in overrides: match override.split("=", 1): case [switch]: From fcc8a8b5148aefef7015e34dfdb78eadf5bcc6b6 Mon Sep 17 00:00:00 2001 From: Matthieu Monsch Date: Mon, 22 Sep 2025 09:43:53 -0700 Subject: [PATCH 3/5] fixup! 492fd69 --- docs/git-draft.1.adoc | 6 ++++++ tests/git_draft/bots/__init___test.py | 17 ++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/docs/git-draft.1.adoc b/docs/git-draft.1.adoc index a117898..06a978d 100644 --- a/docs/git-draft.1.adoc +++ b/docs/git-draft.1.adoc @@ -66,6 +66,12 @@ If a bot needs more information, its question will be persisted and displayed wh Set bot name. The default is to use the first bot defined in the configuration. +-o OPTION:: +--bot-option=OPTION:: +Set bot option. +Each option has the format `[=]` (if the value is omitted, it will default to true). +This will override any option set in configuration files. + -e:: --edit:: Enable interactive editing of draft prompts and templates. diff --git a/tests/git_draft/bots/__init___test.py b/tests/git_draft/bots/__init___test.py index 3fa76fc..f28d494 100644 --- a/tests/git_draft/bots/__init___test.py +++ b/tests/git_draft/bots/__init___test.py @@ -8,7 +8,9 @@ class FakeBot(sut.Bot): - pass + def __init__(self, key: str="default", switch: bool=False) -> None: + self.key = key + self.switch = switch class TestLoadBot: @@ -20,8 +22,17 @@ def import_module(name): monkeypatch.setattr(importlib, "import_module", import_module) config = BotConfig(factory="fake_module:FakeBot") - bot = sut.load_bot(config) - assert isinstance(bot, FakeBot) + + bot0 = sut.load_bot(config) + assert isinstance(bot0, FakeBot) + assert bot0.key == "default" + assert not bot0.switch + + bot1 = sut.load_bot(config, overrides=["key=one", "switch"]) + assert isinstance(bot1, FakeBot) + assert bot1.key == "one" + assert bot1.switch + def test_non_existing_factory(self) -> None: config = BotConfig("git_draft:unknown_factory") From c5a7447ee6f4b0c704c6e4c9769c7b7648d68030 Mon Sep 17 00:00:00 2001 From: Matthieu Monsch Date: Mon, 22 Sep 2025 10:15:20 -0700 Subject: [PATCH 4/5] fixup! fcc8a8b --- src/git_draft/bots/__init__.py | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/src/git_draft/bots/__init__.py b/src/git_draft/bots/__init__.py index 00595fc..bbfbc31 100644 --- a/src/git_draft/bots/__init__.py +++ b/src/git_draft/bots/__init__.py @@ -2,7 +2,6 @@ from collections.abc import Sequence import importlib -import os import sys from ..common import ( @@ -64,29 +63,16 @@ def _parse_overrides(overrides: Sequence[str]) -> JSONObject: def _default_bot(options: JSONObject) -> Bot: - if not os.environ.get("OPENAI_API_KEY"): - raise RuntimeError( - reindent( - """ - The default bot implementation requires an OpenAI API key. - Please specify one via the `$OPENAI_API_KEY` environment - variable or enable a different bot in your configuration. - """ - ) - ) - try: from .openai_api import new_completions_bot except ImportError: raise RuntimeError( - reindent( - """ - The default bot implementation requires the `openai` Python - package. Please install it or specify a different bot in - your configuration. - """ - ) + reindent(""" + The default bot implementation requires the `openai` Python + package. Please install it or specify a different bot in + your configuration. + """) ) else: return new_completions_bot(**options) From dda64184d7b09da148586a254d76fa270394c617 Mon Sep 17 00:00:00 2001 From: Matthieu Monsch Date: Mon, 22 Sep 2025 10:16:29 -0700 Subject: [PATCH 5/5] fixup! c5a7447 --- tests/git_draft/bots/__init___test.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/git_draft/bots/__init___test.py b/tests/git_draft/bots/__init___test.py index f28d494..9401b06 100644 --- a/tests/git_draft/bots/__init___test.py +++ b/tests/git_draft/bots/__init___test.py @@ -38,8 +38,3 @@ def test_non_existing_factory(self) -> None: config = BotConfig("git_draft:unknown_factory") with pytest.raises(NotImplementedError): sut.load_bot(config) - - def test_default_no_key(self, monkeypatch) -> None: - monkeypatch.setenv("OPENAI_API_KEY", "") - with pytest.raises(RuntimeError): - sut.load_bot(None)