diff --git a/features/steps/tui_first_run_steps.py b/features/steps/tui_first_run_steps.py index 590b4dead..a89d7b020 100644 --- a/features/steps/tui_first_run_steps.py +++ b/features/steps/tui_first_run_steps.py @@ -488,3 +488,133 @@ 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}" ) + + +# ----------------------------------------------------------------------- +# Auto-generated presets from actor schema +# ----------------------------------------------------------------------- + + +@given('an actor service with a mock actor "{actor}" with thinking_effort argument') +def step_actor_service_with_thinking_effort(context: object, actor: str) -> None: + from cleveragents.domain.models.core import Actor + + mock_actor = MagicMock(spec=Actor) + mock_actor.name = actor + mock_actor.config_blob = { + "options": { + "thinking_effort": { + "type": "string", + "enum": ["low", "high", "max"], + } + } + } + + mock_service = MagicMock() + mock_service.get_actor.return_value = mock_actor + context._actor_service = mock_service + + +@given('an actor service with a mock actor "{actor}" with temperature argument') +def step_actor_service_with_temperature(context: object, actor: str) -> None: + from cleveragents.domain.models.core import Actor + + mock_actor = MagicMock(spec=Actor) + mock_actor.name = actor + mock_actor.config_blob = { + "options": { + "temperature": { + "type": "number", + "minimum": 0.0, + "maximum": 2.0, + } + } + } + + mock_service = MagicMock() + mock_service.get_actor.return_value = mock_actor + context._actor_service = mock_service + + +@given( + 'an actor service with a mock actor "{actor}" with thinking_effort and temperature arguments' +) +def step_actor_service_with_both(context: object, actor: str) -> None: + from cleveragents.domain.models.core import Actor + + mock_actor = MagicMock(spec=Actor) + mock_actor.name = actor + mock_actor.config_blob = { + "options": { + "thinking_effort": { + "type": "string", + "enum": ["low", "high", "max"], + }, + "temperature": { + "type": "number", + "minimum": 0.0, + "maximum": 2.0, + }, + } + } + + mock_service = MagicMock() + mock_service.get_actor.return_value = mock_actor + context._actor_service = mock_service + + +@given('an actor service with a mock actor "{actor}" with no recognized arguments') +def step_actor_service_with_no_args(context: object, actor: str) -> None: + from cleveragents.domain.models.core import Actor + + mock_actor = MagicMock(spec=Actor) + mock_actor.name = actor + mock_actor.config_blob = { + "options": { + "custom_param": { + "type": "string", + } + } + } + + mock_service = MagicMock() + mock_service.get_actor.return_value = mock_actor + context._actor_service = mock_service + + +@given("an actor service that raises an exception on get_actor") +def step_actor_service_raises_exception(context: object) -> None: + mock_service = MagicMock() + mock_service.get_actor.side_effect = RuntimeError("Actor not found") + context._actor_service = mock_service + + +@when('I call create_default_persona_for_actor with actor "{actor}" and actor_service') +def step_create_persona_with_service(context: object, actor: str) -> None: + from cleveragents.tui.first_run import create_default_persona_for_actor + + context._created_persona = create_default_persona_for_actor( + context._registry, actor, actor_service=context._actor_service + ) + + +@then('the default persona should have preset "{preset_name}"') +def step_persona_has_preset(context: object, preset_name: str) -> None: + persona = context._registry.get("default") + assert persona is not None, "Expected persona 'default' in registry" + preset_names = [p.name for p in persona.argument_presets] + assert preset_name in preset_names, ( + f"Expected preset '{preset_name}' in persona presets, got: {preset_names}" + ) + + +@then("the default persona should have exactly {count:d} preset") +@then("the default persona should have exactly {count:d} presets") +def step_persona_has_exact_presets(context: object, count: int) -> None: + persona = context._registry.get("default") + assert persona is not None, "Expected persona 'default' in registry" + actual_count = len(persona.argument_presets) + assert actual_count == count, ( + f"Expected {count} preset(s), got {actual_count}: " + f"{[p.name for p in persona.argument_presets]}" + ) diff --git a/features/tui_first_run.feature b/features/tui_first_run.feature index 20e18d55d..c94673ede 100644 --- a/features/tui_first_run.feature +++ b/features/tui_first_run.feature @@ -194,3 +194,60 @@ Feature: TUI first-run experience with actor selection overlay And I call _complete_first_run with actor "anthropic/claude-4-sonnet" Then the registry should contain a persona named "default" And the persona bar should reflect the new actor + + # ----------------------------------------------------------------------- + # Auto-generated presets from actor schema + # ----------------------------------------------------------------------- + + Scenario: create_default_persona_for_actor auto-generates presets for actor with thinking_effort + Given an empty persona registry + And an actor service with a mock actor "anthropic/claude-4-sonnet" with thinking_effort argument + When I call create_default_persona_for_actor with actor "anthropic/claude-4-sonnet" and actor_service + Then the registry should contain a persona named "default" + And the default persona should have preset "default" + And the default persona should have preset "think: high" + And the default persona should have preset "think: max" + And the default persona should have exactly 3 presets + + Scenario: create_default_persona_for_actor auto-generates presets for actor with temperature + Given an empty persona registry + And an actor service with a mock actor "openai/gpt-4o" with temperature argument + When I call create_default_persona_for_actor with actor "openai/gpt-4o" and actor_service + Then the registry should contain a persona named "default" + And the default persona should have preset "default" + And the default persona should have preset "precise" + And the default persona should have preset "creative" + + Scenario: create_default_persona_for_actor auto-generates presets for actor with both thinking_effort and temperature + Given an empty persona registry + And an actor service with a mock actor "anthropic/claude-4-sonnet" with thinking_effort and temperature arguments + When I call create_default_persona_for_actor with actor "anthropic/claude-4-sonnet" and actor_service + Then the registry should contain a persona named "default" + And the default persona should have preset "default" + And the default persona should have preset "think: high" + And the default persona should have preset "think: max" + And the default persona should have preset "precise" + And the default persona should have preset "creative" + + Scenario: create_default_persona_for_actor creates only default preset for actor with no recognized arguments + Given an empty persona registry + And an actor service with a mock actor "local/mock" with no recognized arguments + When I call create_default_persona_for_actor with actor "local/mock" and actor_service + Then the registry should contain a persona named "default" + And the default persona should have preset "default" + And the default persona should have exactly 1 preset + + Scenario: create_default_persona_for_actor without actor_service creates only default preset + Given an empty persona registry + When I call create_default_persona_for_actor with actor "anthropic/claude-4-sonnet" + Then the registry should contain a persona named "default" + And the default persona should have preset "default" + And the default persona should have exactly 1 preset + + Scenario: create_default_persona_for_actor falls back to default preset when actor lookup fails + Given an empty persona registry + And an actor service that raises an exception on get_actor + When I call create_default_persona_for_actor with actor "anthropic/claude-4-sonnet" and actor_service + Then the registry should contain a persona named "default" + And the default persona should have preset "default" + And the default persona should have exactly 1 preset diff --git a/src/cleveragents/tui/first_run.py b/src/cleveragents/tui/first_run.py index dd956b42f..31db030cc 100644 --- a/src/cleveragents/tui/first_run.py +++ b/src/cleveragents/tui/first_run.py @@ -2,8 +2,13 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from cleveragents.tui.persona.registry import PersonaRegistry -from cleveragents.tui.persona.schema import Persona +from cleveragents.tui.persona.schema import Persona, PersonaPreset + +if TYPE_CHECKING: + from cleveragents.application.services.actor_service import ActorService def is_first_run(registry: PersonaRegistry) -> bool: @@ -27,9 +32,90 @@ def is_first_run(registry: PersonaRegistry) -> bool: return len(registry.list_personas()) == 0 +def _auto_generate_presets( + actor_service: ActorService, + actor: str, +) -> list[PersonaPreset]: + """Auto-generate presets from actor schema. + + Inspects the actor's argument schema to detect thinking_effort and + temperature arguments, then generates appropriate presets. + + Parameters + ---------- + actor_service: + The :class:`~cleveragents.application.services.actor_service.ActorService` + instance to use for actor lookup. + actor: + Fully-qualified actor reference (e.g. ``"anthropic/claude-4-sonnet"``). + + Returns + ------- + list[PersonaPreset] + List of auto-generated presets. Always includes "default" preset. + May include "think: high", "think: max", "precise", and "creative" + presets depending on actor schema. + """ + presets: list[PersonaPreset] = [ + PersonaPreset(name="default", display="default", overrides={}) + ] + + try: + actor_obj = actor_service.get_actor(actor) + except Exception: + # If actor lookup fails, return only default preset + return presets + + # Inspect actor's config_blob for options schema + config_blob = actor_obj.config_blob or {} + options = config_blob.get("options", {}) + + if not isinstance(options, dict): + return presets + + # Check for thinking_effort argument + has_thinking_effort = "thinking_effort" in options + if has_thinking_effort: + 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"}, + ) + ) + + # Check for temperature argument + has_temperature = "temperature" in options + if has_temperature: + presets.append( + PersonaPreset( + name="precise", + display="precise", + overrides={"temperature": 0.0}, + ) + ) + presets.append( + PersonaPreset( + name="creative", + display="creative", + overrides={"temperature": 1.0}, + ) + ) + + return presets + + def create_default_persona_for_actor( registry: PersonaRegistry, actor: str, + actor_service: ActorService | None = None, ) -> Persona: """Create and persist a ``"default"`` persona for the given actor. @@ -37,6 +123,9 @@ 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. + If an actor_service is provided, auto-generates presets based on the + actor's schema (thinking_effort and temperature arguments). + Parameters ---------- registry: @@ -44,16 +133,28 @@ def create_default_persona_for_actor( instance to write to. actor: Fully-qualified actor reference (e.g. ``"anthropic/claude-4-sonnet"``). + actor_service: + Optional :class:`~cleveragents.application.services.actor_service.ActorService` + instance for auto-generating presets from actor schema. Returns ------- Persona The newly created and persisted default persona. """ + # Auto-generate presets if actor_service is provided + presets: list[PersonaPreset] = [] + if actor_service is not None: + presets = _auto_generate_presets(actor_service, actor) + else: + # Default to just the default preset + presets = [PersonaPreset(name="default", display="default", overrides={})] + persona = Persona( name="default", actor=actor, description="Default persona", + argument_presets=presets, ) registry.save(persona) registry.set_last_persona(persona.name)