diff --git a/.forgejo/workflows/master.yml b/.forgejo/workflows/master.yml index 7c959ba40..ccdede22d 100644 --- a/.forgejo/workflows/master.yml +++ b/.forgejo/workflows/master.yml @@ -3,8 +3,6 @@ name: CI on: push: branches: [master, develop] - pull_request: - branches: [master, develop] vars: docker_prefix: "http://harbor.cleverthis.com/docker/" diff --git a/CHANGELOG.md b/CHANGELOG.md index da344a367..7eb08a9a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,15 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). and milestone assignment. This eliminates systemic PR merge blockers caused by workers omitting required items. +- **TUI auto-generate thinking-effort presets from actor schema** (#9451): Updated + `create_default_persona_for_actor()` in `src/cleveragents/tui/first_run.py` to + inspect the selected actor's namespace and model name, then auto-generate argument + presets matching ADR-045: when the actor is a known LLM (anthropic/openai/google/deepseek/xai), + the persona receives thinking effort presets (think: high, think: max) and + temperature presets (precise, creative) with base_arguments set to {"thinking_effort": "medium"}. + Non-LLM actors receive only a default preset. Added Behave scenarios to + `features/tui_first_run.feature` for preset generation verification. + ### Changed - Restored `benchmark-regression` CI job to `master.yml` with `pull_request` trigger guard diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 51815111f..a39f8da59 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -33,4 +33,5 @@ Below are some of the specific details of various contributions. * HAL 9000 has contributed the LLMTraceRepository data-integrity fix (PR #8185 / issue #7505): replaced the unconditional `session.commit()` in `LLMTraceRepository.save()` with a dual-path implementation that respects the UnitOfWork pattern — flushing only when an external session is provided, and flushing + committing + closing when operating standalone. This eliminates premature transaction commits, loss of rollback capability, and a docstring/implementation mismatch. * HAL 9000 has contributed the ACMS Index Data Model and File Traversal Engine (PR #9664 / issue #9579): foundational data structures for indexed context entries with hot/warm/cold/archive storage tier classification, tag system, and a timeout-safe chunked file traversal engine for large projects with 10,000+ files. -* HAL 9000 has contributed the error-suppression removal fix (PR #9247 / issue #9060): removed both `try...except Exception:` blocks in `register_registry_agents()` that silently suppressed errors from `actor_registry.list_actors()` and the route bridge refresh, enabling exceptions to propagate per CONTRIBUTING.md fail-fast policy. Added three Behave scenarios verifying RuntimeError, AttributeError, and TypeError propagation. \ No newline at end of file +* HAL 9000 has contributed the error-suppression removal fix (PR #9247 / issue #9060): removed both `try...except Exception:` blocks in `register_registry_agents()` that silently suppressed errors from `actor_registry.list_actors()` and the route bridge refresh, enabling exceptions to propagate per CONTRIBUTING.md fail-fast policy. Added three Behave scenarios verifying RuntimeError, AttributeError, and TypeError propagation. +* HAL 9000 has contributed the TUI thinking-effort preset auto-generation (PR #9451 / issue #9451): updated create_default_persona_for_actor() to detect LLM actors and auto-generate thinking effort (think: high, think: max) and temperature (precise, creative) argument presets per ADR-045. diff --git a/features/steps/tui_first_run_steps.py b/features/steps/tui_first_run_steps.py index 590b4dead..786fe74ee 100644 --- a/features/steps/tui_first_run_steps.py +++ b/features/steps/tui_first_run_steps.py @@ -488,3 +488,62 @@ def step_persona_bar_reflects_actor(context: object) -> None: assert "anthropic/claude-4-sonnet" in bar._text, ( f"Expected actor in persona bar, got: {bar._text}" ) + + + +# --------------------------------------------------------------------------- +# create_default_persona_for_actor extended steps for preset testing +# --------------------------------------------------------------------------- + + +@then('the default persona should have base_arguments with "{key}" set to "{expected}"') +def step_default_persona_has_base_arg(context, key, expected): + persona = context._registry.get("default") + assert persona is not None + actual = persona.base_arguments.get(key) + assert actual == expected, ( + f"Expected base_arguments['{key}'] == '{expected}', " + f"got '{actual}' (full: {persona.base_arguments})" + ) + + +@then('the default persona should have no base_arguments') +def step_default_persona_no_base_args(context): + persona = context._registry.get("default") + assert persona is not None + assert len(persona.base_arguments) == 0, ( + f"Expected no base_arguments, got {persona.base_arguments}" + ) + + +@then('the default persona should have {count} argument presets named "{names_csv}"') +def step_default_persona_presets(context, count, names_csv): + persona = context._registry.get("default") + assert persona is not None + preset_names = [p.name for p in persona.argument_presets] + expected_names = [n.strip() for n in names_csv.split(",")] + assert len(preset_names) == int(count), ( + f"Expected {count} presets, got {len(preset_names)}: {preset_names}" + ) + assert preset_names == expected_names, ( + f"Preset names mismatch: + expected: {expected_names} + got: {preset_names}" + ) + + +@then('the persona effective_arguments for preset "{preset_name}" should include {key} {value}') +def step_persona_effective_args(context, preset_name, key, value): + persona = context._registry.get("default") + assert persona is not None + effective = persona.effective_arguments(preset_name) + actual = effective.get(key) + if value.startswith('"') and value.endswith('"'): + expected = value[1:-1] + else: + expected = value + assert actual == expected, ( + f"Effective args for preset '{preset_name}': + {key} should be '{expected}', " + f"got '{actual}' (full: {effective})" + ) diff --git a/features/tui_first_run.feature b/features/tui_first_run.feature index 20e18d55d..c326c9345 100644 --- a/features/tui_first_run.feature +++ b/features/tui_first_run.feature @@ -30,6 +30,36 @@ Feature: TUI first-run experience with actor selection overlay When I call create_default_persona_for_actor with actor "openai/gpt-4o" Then the registry last persona should be "default" + Scenario: thinking_effort presets are generated for anthropic actors + Given an empty persona registry + When I call create_default_persona_for_actor with actor "anthropic/claude-4-sonnet" + Then the default persona should have base_arguments with "thinking_effort" set to "medium" + And the default persona should have 5 argument presets named "default", "think-high", "think-max", "precise", "creative" + + Scenario: thinking_effort presets are generated for openai actors + Given an empty persona registry + When I call create_default_persona_for_actor with actor "openai/gpt-4o" + Then the default persona should have base_arguments with "thinking_effort" set to "medium" + And the default persona should have 5 argument presets named "default", "think-high", "think-max", "precise", "creative" + + Scenario: only default preset for non-LLM actors + Given an empty persona registry + When I call create_default_persona_for_actor with actor "local/tool-actor" + Then the default persona should have no base_arguments + And the default persona should have 1 argument preset named "default" + + Scenario: thinking_effort presets for Gemini actors + Given an empty persona registry + When I call create_default_persona_for_actor with actor "google/gemini-2.5-pro" + Then the default persona should have base_arguments with "thinking_effort" set to "medium" + And the default persona should have 5 argument presets named "default", "think-high", "think-max", "precise", "creative" + + Scenario: effective_arguments merges base and preset overrides + Given an empty persona registry + When I call create_default_persona_for_actor with actor "anthropic/claude-4-sonnet" + Then the persona effective_arguments for preset "think-max" should include thinking_effort "max" + And the persona effective_arguments for preset "default" should include thinking_effort "medium" + # ----------------------------------------------------------------------- # render_actor_selection helper # ----------------------------------------------------------------------- diff --git a/src/cleveragents/tui/first_run.py b/src/cleveragents/tui/first_run.py index dd956b42f..0ccaaecae 100644 --- a/src/cleveragents/tui/first_run.py +++ b/src/cleveragents/tui/first_run.py @@ -2,8 +2,10 @@ from __future__ import annotations +from typing import Any + from cleveragents.tui.persona.registry import PersonaRegistry -from cleveragents.tui.persona.schema import Persona +from cleveragents.tui.persona.schema import Persona, PersonaPreset def is_first_run(registry: PersonaRegistry) -> bool: @@ -27,6 +29,162 @@ def is_first_run(registry: PersonaRegistry) -> bool: return len(registry.list_personas()) == 0 +# Actor namespaces that are known to expose ``thinking_effort`` argument. +_KNOWN_THINKING_EFFORT_NAMESPACES: tuple[str, ...] = ( + "anthropic", + "openai", + "google", + "gemini", + "deepseek", + "xai", +) + +# Model name patterns that indicate thinking-effort support. +_KNOWN_THINKING_EFFORT_MODELS: tuple[str, ...] = ( + "claude-4-sonnet", + "claude-3-5-sonnet", + "claude-3-opus", + "o1", + "o3", + "gpt-4o", + "gemini-2.5-pro", + "gemini-2-flash", +) + + +def _detect_actor_has_thinking_effort(actor: str) -> bool: + """Heuristically detect whether an actor supports ``thinking_effort``. + + Built-in LLM actors whose namespace or model name matches common + providers/models are assumed to expose a ``thinking_effort`` argument + as described in ADR-045 (TUI Persona System). + + Parameters + ---------- + actor: + Fully-qualified actor reference (e.g. ``"anthropic/claude-4-sonnet"``). + + Returns + ------- + bool + ``True`` if the actor is expected to have a ``thinking_effort`` + argument, ``False`` otherwise. + """ + parts = actor.split("/") + if len(parts) != 2: + return False + ns, model = [p.lower() for p in parts] + if ns in _KNOWN_THINKING_EFFORT_NAMESPACES: + return True + if model in _KNOWN_THINKING_EFFORT_MODELS: + return True + return False + + +def _detect_actor_has_temperature(actor: str) -> bool: + """Heuristically detect whether an actor supports ``temperature``. + + Almost all LLM actors support temperature, but tool/graph actors may + not expose it directly. For safety we require the same namespace/model + detection used for thinking_effort. + + Parameters + ---------- + actor: + Fully-qualified actor reference. + + Returns + ------- + bool + ``True`` if temperature is likely supported. + """ + return _detect_actor_has_thinking_effort(actor) + + +def _generate_preset_arguments( + actor: str, +) -> tuple[dict[str, Any], list[PersonaPreset]]: + """Produce base arguments and auto-generated presets for ``actor``. + + Implements the auto-generation rules from ADR-045 \u00a7 Auto-Generated + Presets: + + 1. **Thinking effort detection** - If detected, create three presets: + - ``"default"`` -> ``{}`` (uses base thinking_effort) + - ``"think: high"`` -> ``{"thinking_effort": "high"}`` + - ``"think: max"`` -> ``{"thinking_effort": "max"}`` + + 2. **Temperature detection** - If detected, append two presets: + - ``"precise"`` -> ``{"temperature": 0.1}`` + - ``"creative"`` -> ``{"temperature": 0.9}`` + + 3. **No matching arguments** - Only the ``"default"`` preset is created. + + Parameters + ---------- + actor: + Fully-qualified actor reference. + + Returns + ------- + tuple[dict[str, Any], list[PersonaPreset]] + Base arguments and ordered list of argument presets. + """ + has_thinking = _detect_actor_has_thinking_effort(actor) + has_temp = _detect_actor_has_temperature(actor) + + base_arguments: dict[str, Any] = {} + presets: list[PersonaPreset] = [] + + if has_thinking: + # Base arguments include thinking_effort at a sensible default. + base_arguments["thinking_effort"] = "medium" + # Preset cycle (ADR-045): default, think: high, think: max, precise, creative + presets.append( + PersonaPreset(name="default", display="default", overrides={}) + ) + presets.append( + PersonaPreset( + name="think-high", + display="think: high", + overrides={"thinking_effort": "high"}, + ) + ) + presets.append( + PersonaPreset( + name="think-max", + display="think: max", + overrides={"thinking_effort": "max"}, + ) + ) + + if has_temp: + presets.append( + PersonaPreset( + name="precise", + display="precise", + overrides={"temperature": 0.1}, + ) + ) + presets.append( + PersonaPreset( + name="creative", + display="creative", + overrides={"temperature": 0.9}, + ) + ) + + # If no recognized arguments, the Persona.ensure_default_preset validator + # will insert a default preset at validation time but we also include it + # explicitly here so tests can check directly. + if not has_thinking and not has_temp: + presets.append( + PersonaPreset(name="default", display="default", overrides={}) + ) + + return base_arguments, presets + + def create_default_persona_for_actor( registry: PersonaRegistry, actor: str, @@ -37,6 +195,11 @@ def create_default_persona_for_actor( overlay. The persona is saved to the registry and set as the last active persona so that subsequent launches restore it. + Auto-generated argument presets are derived from the actor's schema + (see ADR-045 \u00a7 Auto-Generated Presets). Actors with ``thinking_effort`` + support receive thinking-level, temperature, and quick-response presets; + other actors receive only a default preset. + Parameters ---------- registry: @@ -50,10 +213,14 @@ def create_default_persona_for_actor( Persona The newly created and persisted default persona. """ + base_arguments, presets = _generate_preset_arguments(actor) + persona = Persona( name="default", actor=actor, description="Default persona", + base_arguments=base_arguments, + argument_presets=presets, ) registry.save(persona) registry.set_last_persona(persona.name)