From 79b8aa5e4a4a134408206818801537439af70233 Mon Sep 17 00:00:00 2001 From: Luca Belli <129434630+sator-labs@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:43:55 -0700 Subject: [PATCH 1/4] refactor: expose resolved generation function --- generate.py | 219 ++++-------------- generate_conversations/__init__.py | 3 +- generate_conversations/run.py | 144 ++++++++++++ run_pipeline.py | 18 +- .../test_generate_cli.py | 135 +++++------ 5 files changed, 256 insertions(+), 263 deletions(-) create mode 100644 generate_conversations/run.py diff --git a/generate.py b/generate.py index f38d7f7ba..0b247756c 100644 --- a/generate.py +++ b/generate.py @@ -2,19 +2,12 @@ import argparse import asyncio -import os import sys -from datetime import datetime from typing import Any, Dict, List, Optional -from generate_conversations import ConversationRunner +from generate_conversations import run_generation from llm_clients.llm_interface import DEFAULT_START_PROMPT from utils.debug import set_debug -from utils.naming import ( - build_generation_run_folder_name, - model_token_for_run_folder, - parse_generation_run_folder_name, -) from utils.rubric_manifest import ( load_manifest_persona_context_template, load_manifest_personas, @@ -23,158 +16,38 @@ async def main( + *, persona_model_config: Dict[str, Any], agent_model_config: Dict[str, Any], - persona_extra_run_params: Dict[str, Any] = {}, - agent_extra_run_params: Dict[str, Any] = {}, - max_turns: int = 3, - runs_per_prompt: int = 2, - persona_names: Optional[List[str]] = None, - verbose: bool = True, - output_folder: Optional[str] = None, - run_id: Optional[str] = None, - max_concurrent: Optional[int] = None, - max_total_words: Optional[int] = None, - max_personas: Optional[int] = None, - persona_speaks_first: bool = True, - session_types: Optional[List[str]] = None, - resume: bool = False, - rubric_manifest: Optional[str] = None, + persona_files: List[str], + persona_extra_run_params: Dict[str, Any], + agent_extra_run_params: Dict[str, Any], + max_turns: int, + runs_per_prompt: int, + persona_names: Optional[List[str]], + verbose: bool, + output_folder: str, + run_id: Optional[str], + max_concurrent: Optional[int], + max_total_words: Optional[int], + max_personas: Optional[int], + persona_speaks_first: bool, + session_types: Optional[List[str]], + resume: bool, + persona_context_template_path: str, ) -> tuple[List[Dict[str, Any]], str]: - """ - Generate conversations and return results. - - Args: - # TODO: should the extra config be separated? - persona_model_config: Configuration dictionary for the persona model - agent_model_config: Configuration dictionary for the agent model - persona_extra_run_params: Extra parameters for the persona model - agent_extra_run_params: Extra parameters for the agent model - max_turns: Maximum turns per conversation - runs_per_prompt: Number of runs per prompt - persona_names: List of persona names to use. If None, uses all personas. - verbose: Whether to print status messages - output_folder: Parent directory for new runs (default ``output/``), or the - existing ``p_*`` run folder when ``resume`` is True. - max_total_words: Optional maximum total words across all responses - max_concurrent: Maximum number of concurrent conversations. If None, runs all - conversations concurrently. - max_personas: Optional maximum number of personas to load from CSV. If None, - loads all personas. - persona_speaks_first: If True (default), persona speaks first; else provider - speaks first. max_turns is adjusted so the provider always speaks last. - rubric_manifest: Optional path to a rubric bundle manifest (see - docs/architecture.md#rubric-bundle-manifest). When set, personas load - from the manifest's ``personas`` list instead of the default - ``data/SI/personas.tsv`` -- Phase 0's generation-side counterpart to - ``judge.py --rubrics``, so a manifest attaches personas and rubric - together. Only the first entry is used if the manifest lists more - than one. - - Returns: - List of conversation results - - Raises: - ValueError: Configuration error - Exception: Other errors - """ - if verbose: - print("🔄 Generating conversations with the following parameters:") - print(f" - Persona model: {persona_model_config}") - print(f" - Agent model: {agent_model_config}") - print(f" - Persona extra run params: {persona_extra_run_params}") - print(f" - Agent extra run params: {agent_extra_run_params}") - print(f" - Max turns: {max_turns}") - print(f" - Runs per prompt: {runs_per_prompt}") - print(f" - Persona names: {persona_names}") - print(f" - Output folder: {output_folder}") - print(f" - Run ID: {run_id}") - print(f" - Max concurrent: {max_concurrent}") - print(f" - Max total words: {max_total_words}") - print(f" - Max personas: {max_personas}") - print(f" - Persona speaks first: {persona_speaks_first}") - print(f" - Resume: {resume}") - - # Generate default folder name if not provided - if output_folder is None: - output_folder = "output" - - persona_prompt_path = "data/SI/personas.tsv" - persona_context_template_path = "data/SI/persona_context_template.txt" - if rubric_manifest: - manifest_personas = await load_manifest_personas(rubric_manifest) - if not manifest_personas: - raise ValueError( - f"Rubric bundle manifest {rubric_manifest} has no personas listed" - ) - if len(manifest_personas) > 1: - print( - f"Warning: manifest lists multiple persona files " - f"({manifest_personas}); multi-persona-file support is not yet " - f"implemented, using only the first: {manifest_personas[0]}", - file=sys.stderr, - ) - persona_prompt_path = manifest_personas[0] - persona_context_template_path = await load_manifest_persona_context_template( - rubric_manifest - ) - - if resume: - if not os.path.isdir(output_folder): - raise ValueError( - "Resume mode requires --output to point to an existing run folder." - ) - run_folder_name = os.path.basename(os.path.normpath(output_folder)) - run_meta = parse_generation_run_folder_name(run_folder_name) - expected_persona = model_token_for_run_folder(persona_model_config["model"]) - expected_agent = model_token_for_run_folder(agent_model_config["model"]) - - if run_meta["persona"] != expected_persona: - raise ValueError( - "Resume folder persona model does not match current --user-agent. " - f"Expected p_{expected_persona}, got p_{run_meta['persona']}." - ) - if run_meta["agent"] != expected_agent: - raise ValueError( - "Resume folder provider model does not match current --provider-agent. " - f"Expected a_{expected_agent}, got a_{run_meta['agent']}." - ) - if run_meta["turns"] != max_turns: - raise ValueError( - "Resume folder max turns does not match current --turns. " - f"Expected t{max_turns}, got t{run_meta['turns']}." - ) - if run_meta["runs"] != runs_per_prompt: - raise ValueError( - "Resume folder runs-per-prompt does not match current --runs. " - f"Expected r{runs_per_prompt}, got r{run_meta['runs']}." - ) - if run_id is None: - run_id = run_folder_name - elif run_id != run_folder_name: - raise ValueError( - "Resume mode requires --run-id to match the run folder name when set." - ) - elif run_id is None: - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - run_id = build_generation_run_folder_name( - persona_model_config["model"], - agent_model_config["model"], - max_turns, - runs_per_prompt, - timestamp, - ) - output_folder = f"{output_folder}/{run_id}" - # TODO: do we want to give a message if the folder already exists? - os.makedirs(output_folder, exist_ok=True) - - # Configuration - runner = ConversationRunner( + """Generate conversations from fully resolved inputs.""" + return await run_generation( persona_model_config=persona_model_config, agent_model_config=agent_model_config, + persona_files=persona_files, + persona_extra_run_params=persona_extra_run_params, + agent_extra_run_params=agent_extra_run_params, max_turns=max_turns, runs_per_prompt=runs_per_prompt, - folder_name=output_folder, + persona_names=persona_names, + verbose=verbose, + output_folder=output_folder, run_id=run_id, max_concurrent=max_concurrent, max_total_words=max_total_words, @@ -182,22 +55,17 @@ async def main( persona_speaks_first=persona_speaks_first, session_types=session_types, resume=resume, - persona_prompt_path=persona_prompt_path, persona_context_template_path=persona_context_template_path, ) - # Run conversations - results = await runner.run_conversations(persona_names=persona_names) - - if verbose: - skipped_n = sum(1 for r in results if r.get("skipped")) - ok_n = len(results) - skipped_n - msg = f"✅ Generated {ok_n} conversations → {output_folder}/" - if skipped_n: - msg += f" ({skipped_n} skipped)" - print(msg) - return results, output_folder +async def resolve_persona_inputs(manifest: str) -> tuple[List[str], str]: + """Resolve generation inputs for legacy callers of ``generate.py``.""" + persona_files = await load_manifest_personas(manifest) + if not persona_files: + raise ValueError(f"Rubric bundle manifest {manifest} has no personas listed") + context_template = await load_manifest_persona_context_template(manifest) + return persona_files, context_template if __name__ == "__main__": @@ -282,7 +150,7 @@ async def main( parser.add_argument( "--output", "-o", - default=None, + default="output", help=( "Parent directory where a new p_*__a_*__t*__r*__* run folder is created " "(default: output). With --resume, must be the existing run folder path." @@ -382,11 +250,9 @@ def parse_sessions_arg(s: str) -> List[str]: "--rubric-manifest", help=( "Rubric bundle manifest to load personas from (see " - "docs/architecture.md#rubric-bundle-manifest), instead of the " - "default data/SI/personas.tsv. Phase 0 stopgap: attaches a rubric's " - "intended personas to a generate.py run ahead of vera.py's --target." + "docs/architecture.md#target-manifest). Defaults to the SI bundle." ), - default=None, + default="data/SI/rubric_manifest.json", ) args = parser.parse_args() @@ -414,11 +280,15 @@ def parse_sessions_arg(s: str) -> List[str]: agent_model_config["first_message"] = args.provider_first_message agent_model_config["start_prompt"] = args.provider_start_prompt - # TODO: Do the run id here, so that it can be printed when starting + persona_files, context_template = asyncio.run( + resolve_persona_inputs(args.rubric_manifest) + ) + results, output_folder = asyncio.run( main( persona_model_config=persona_model_config, agent_model_config=agent_model_config, + persona_files=persona_files, max_turns=args.turns, runs_per_prompt=args.runs, persona_extra_run_params={ @@ -447,14 +317,17 @@ def parse_sessions_arg(s: str) -> List[str]: "top_p", ] }, - output_folder=args.output or "output", + persona_names=None, + verbose=True, + output_folder=args.output, + run_id=args.run_id, max_concurrent=args.max_concurrent, max_total_words=args.max_total_words, max_personas=args.max_personas, persona_speaks_first=not args.provider_speaks_first, session_types=args.sessions, resume=args.resume, - rubric_manifest=args.rubric_manifest, + persona_context_template_path=context_template, ) ) if results and all(r.get("skipped") for r in results): diff --git a/generate_conversations/__init__.py b/generate_conversations/__init__.py index 2538fc825..83b2a1aab 100644 --- a/generate_conversations/__init__.py +++ b/generate_conversations/__init__.py @@ -1,5 +1,6 @@ """Generate Conversations Package - LLM Conversation Simulation""" +from .run import run_generation from .runner import ConversationRunner -__all__ = ["ConversationRunner"] +__all__ = ["ConversationRunner", "run_generation"] diff --git a/generate_conversations/run.py b/generate_conversations/run.py new file mode 100644 index 000000000..c73461382 --- /dev/null +++ b/generate_conversations/run.py @@ -0,0 +1,144 @@ +"""Resolved-input application function for conversation generation.""" + +from __future__ import annotations + +import os +import sys +from datetime import datetime +from typing import Any, Dict, List, Optional + +from utils.naming import ( + build_generation_run_folder_name, + model_token_for_run_folder, + parse_generation_run_folder_name, +) + +from .runner import ConversationRunner + + +async def run_generation( + *, + persona_model_config: Dict[str, Any], + agent_model_config: Dict[str, Any], + persona_files: List[str], + persona_extra_run_params: Dict[str, Any], + agent_extra_run_params: Dict[str, Any], + max_turns: int, + runs_per_prompt: int, + persona_names: Optional[List[str]], + verbose: bool, + output_folder: str, + run_id: Optional[str], + max_concurrent: Optional[int], + max_total_words: Optional[int], + max_personas: Optional[int], + persona_speaks_first: bool, + session_types: Optional[List[str]], + resume: bool, + persona_context_template_path: str, +) -> tuple[List[Dict[str, Any]], str]: + """Generate conversations from already-resolved runtime values.""" + if verbose: + print("🔄 Generating conversations with the following parameters:") + print(f" - Persona model: {persona_model_config}") + print(f" - Agent model: {agent_model_config}") + print(f" - Persona extra run params: {persona_extra_run_params}") + print(f" - Agent extra run params: {agent_extra_run_params}") + print(f" - Max turns: {max_turns}") + print(f" - Runs per prompt: {runs_per_prompt}") + print(f" - Persona names: {persona_names}") + print(f" - Output folder: {output_folder}") + print(f" - Run ID: {run_id}") + print(f" - Max concurrent: {max_concurrent}") + print(f" - Max total words: {max_total_words}") + print(f" - Max personas: {max_personas}") + print(f" - Persona speaks first: {persona_speaks_first}") + print(f" - Resume: {resume}") + + if not persona_files: + raise ValueError("generation requires at least one persona file") + if len(persona_files) > 1: + print( + f"Warning: multiple persona files passed ({persona_files}); " + "multi-persona-file support is not yet implemented, using only " + f"the first: {persona_files[0]}", + file=sys.stderr, + ) + persona_prompt_path = persona_files[0] + + if resume: + if not os.path.isdir(output_folder): + raise ValueError( + "Resume mode requires --output to point to an existing run folder." + ) + run_folder_name = os.path.basename(os.path.normpath(output_folder)) + run_meta = parse_generation_run_folder_name(run_folder_name) + expected_persona = model_token_for_run_folder(persona_model_config["model"]) + expected_agent = model_token_for_run_folder(agent_model_config["model"]) + + if run_meta["persona"] != expected_persona: + raise ValueError( + "Resume folder persona model does not match current --user-agent. " + f"Expected p_{expected_persona}, got p_{run_meta['persona']}." + ) + if run_meta["agent"] != expected_agent: + raise ValueError( + "Resume folder provider model does not match current --provider-agent. " + f"Expected a_{expected_agent}, got a_{run_meta['agent']}." + ) + if run_meta["turns"] != max_turns: + raise ValueError( + "Resume folder max turns does not match current --turns. " + f"Expected t{max_turns}, got t{run_meta['turns']}." + ) + if run_meta["runs"] != runs_per_prompt: + raise ValueError( + "Resume folder runs-per-prompt does not match current --runs. " + f"Expected r{runs_per_prompt}, got r{run_meta['runs']}." + ) + if run_id is None: + run_id = run_folder_name + elif run_id != run_folder_name: + raise ValueError( + "Resume mode requires --run-id to match the run folder name when set." + ) + elif run_id is None: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + run_id = build_generation_run_folder_name( + persona_model_config["model"], + agent_model_config["model"], + max_turns, + runs_per_prompt, + timestamp, + ) + output_folder = f"{output_folder}/{run_id}" + os.makedirs(output_folder, exist_ok=True) + + runner = ConversationRunner( + persona_model_config=persona_model_config, + agent_model_config=agent_model_config, + max_turns=max_turns, + runs_per_prompt=runs_per_prompt, + folder_name=output_folder, + run_id=run_id, + max_concurrent=max_concurrent, + max_total_words=max_total_words, + max_personas=max_personas, + persona_speaks_first=persona_speaks_first, + session_types=session_types, + resume=resume, + persona_prompt_path=persona_prompt_path, + persona_context_template_path=persona_context_template_path, + ) + results = await runner.run_conversations(persona_names=persona_names) + + if verbose: + skipped = sum(1 for result in results if result.get("skipped")) + message = ( + f"✅ Generated {len(results) - skipped} conversations → {output_folder}/" + ) + if skipped: + message += f" ({skipped} skipped)" + print(message) + + return results, output_folder diff --git a/run_pipeline.py b/run_pipeline.py index 1263c6fb6..251bf5541 100644 --- a/run_pipeline.py +++ b/run_pipeline.py @@ -406,11 +406,10 @@ def parse_arguments(): ) parser.add_argument( "--rubric-manifest", - default=None, + default="data/SI/rubric_manifest.json", help=( - "Rubric bundle manifest to load generation personas from (see " - "docs/architecture.md#rubric-bundle-manifest), instead of the " - "default data/SI/personas.tsv. Independent of --rubrics: passing the " + "Target manifest to load generation personas from (default: " + "data/SI/rubric_manifest.json). Independent of --rubrics: passing the " "same manifest to both attaches this run's personas to the rubric " "it's evaluated against." ), @@ -447,6 +446,7 @@ async def main(): import importlib.util from generate import main as generate_main + from generate import resolve_persona_inputs spec = importlib.util.spec_from_file_location("judge_script", "judge.py") judge_script = importlib.util.module_from_spec(spec) @@ -482,10 +482,13 @@ async def main(): agent_model_config["first_message"] = args.provider_first_message agent_model_config["start_prompt"] = args.provider_start_prompt - # Call generate.py's main function directly + persona_files, context_template = await resolve_persona_inputs(args.rubric_manifest) + + # Call generate.py's reusable function directly. _, conversation_folder = await generate_main( persona_model_config=persona_model_config, agent_model_config=agent_model_config, + persona_files=persona_files, max_turns=args.turns, runs_per_prompt=args.runs, persona_extra_run_params={ @@ -498,14 +501,17 @@ async def main(): for k, v in agent_model_config.items() if k not in ["model", "model_name", "name", "temperature", "max_tokens"] }, + persona_names=None, + verbose=True, output_folder=args._pipeline_gen_folder, run_id=args.run_id, max_concurrent=args.max_concurrent, max_total_words=args.max_total_words, max_personas=args.max_personas, persona_speaks_first=not args.provider_speaks_first, + session_types=None, resume=args._pipeline_resume_generate, - rubric_manifest=args.rubric_manifest, + persona_context_template_path=context_template, ) print("") diff --git a/tests/unit/generate_conversations/test_generate_cli.py b/tests/unit/generate_conversations/test_generate_cli.py index c21decd59..8fb488858 100644 --- a/tests/unit/generate_conversations/test_generate_cli.py +++ b/tests/unit/generate_conversations/test_generate_cli.py @@ -1,5 +1,6 @@ """Unit tests for generate.py resume behavior.""" +import inspect import json from pathlib import Path from unittest.mock import AsyncMock, patch @@ -9,27 +10,54 @@ import generate +def _generation_kwargs(output_folder: str, **overrides: object) -> dict: + values = { + "persona_model_config": {"model": "mock-persona"}, + "agent_model_config": {"model": "mock-agent", "name": "mock-agent"}, + "persona_files": ["data/SI/personas.tsv"], + "persona_extra_run_params": {}, + "agent_extra_run_params": {}, + "max_turns": 4, + "runs_per_prompt": 1, + "persona_names": None, + "verbose": False, + "output_folder": output_folder, + "run_id": None, + "max_concurrent": None, + "max_total_words": None, + "max_personas": None, + "persona_speaks_first": True, + "session_types": None, + "resume": False, + "persona_context_template_path": "data/SI/persona_context_template.txt", + } + values.update(overrides) + return values + + +def test_main_requires_every_runtime_value() -> None: + """The reusable generation function must not own CLI behavior defaults.""" + parameters = inspect.signature(generate.main).parameters.values() + + assert all(parameter.default is inspect.Parameter.empty for parameter in parameters) + + @pytest.mark.asyncio async def test_main_resume_uses_existing_run_folder(tmp_path: Path) -> None: """Resume mode should reuse provided run folder and avoid nesting.""" run_folder = tmp_path / "p_mock_persona__a_mock_agent__t4__r1__20260331_120000" run_folder.mkdir(parents=True, exist_ok=True) - persona_model_config = {"model": "mock-persona"} - agent_model_config = {"model": "mock-agent", "name": "mock-agent"} - - with patch("generate.ConversationRunner") as mock_runner_cls: + with patch("generate_conversations.run.ConversationRunner") as mock_runner_cls: mock_runner = mock_runner_cls.return_value mock_runner.run_conversations = AsyncMock(return_value=[]) _, output_folder = await generate.main( - persona_model_config=persona_model_config, - agent_model_config=agent_model_config, - max_turns=4, - runs_per_prompt=1, - output_folder=str(run_folder), - resume=True, - verbose=False, + **_generation_kwargs( + str(run_folder), + resume=True, + persona_model_config={"model": "mock-persona"}, + ) ) assert output_folder == str(run_folder) @@ -45,26 +73,21 @@ async def test_main_resume_mismatch_raises_value_error(tmp_path: Path) -> None: run_folder = tmp_path / "p_mock_persona__a_mock_agent__t4__r1__20260331_120000" run_folder.mkdir(parents=True, exist_ok=True) - persona_model_config = {"model": "different-persona"} - agent_model_config = {"model": "mock-agent", "name": "mock-agent"} - with pytest.raises(ValueError, match="persona model does not match"): await generate.main( - persona_model_config=persona_model_config, - agent_model_config=agent_model_config, - max_turns=4, - runs_per_prompt=1, - output_folder=str(run_folder), - resume=True, - verbose=False, + **_generation_kwargs( + str(run_folder), + resume=True, + persona_model_config={"model": "different-persona"}, + ) ) @pytest.mark.asyncio -async def test_main_rubric_manifest_loads_personas_from_manifest( +async def test_resolve_persona_inputs_loads_manifest_paths( tmp_path: Path, ) -> None: - """--rubric-manifest should select personas from the manifest, not the default.""" + """Legacy callers can resolve persona inputs before calling ``main``.""" manifest_path = tmp_path / "manifest.json" manifest_path.write_text( json.dumps( @@ -79,61 +102,16 @@ async def test_main_rubric_manifest_loads_personas_from_manifest( encoding="utf-8", ) - persona_model_config = {"model": "mock-persona"} - agent_model_config = {"model": "mock-agent", "name": "mock-agent"} - - with patch("generate.ConversationRunner") as mock_runner_cls: - mock_runner = mock_runner_cls.return_value - mock_runner.run_conversations = AsyncMock(return_value=[]) - - await generate.main( - persona_model_config=persona_model_config, - agent_model_config=agent_model_config, - max_turns=4, - runs_per_prompt=1, - output_folder=str(tmp_path / "out"), - run_id="run1", - rubric_manifest=str(manifest_path), - verbose=False, - ) - - kwargs = mock_runner_cls.call_args.kwargs - assert kwargs["persona_prompt_path"] == str(tmp_path / "personas_custom.tsv") - assert kwargs["persona_context_template_path"] == str( - tmp_path / "persona_context_custom.txt" + personas, context_template = await generate.resolve_persona_inputs( + str(manifest_path) ) - -@pytest.mark.asyncio -async def test_main_no_rubric_manifest_uses_default_personas(tmp_path: Path) -> None: - """Omitting --rubric-manifest should keep today's fixed-default persona path.""" - persona_model_config = {"model": "mock-persona"} - agent_model_config = {"model": "mock-agent", "name": "mock-agent"} - - with patch("generate.ConversationRunner") as mock_runner_cls: - mock_runner = mock_runner_cls.return_value - mock_runner.run_conversations = AsyncMock(return_value=[]) - - await generate.main( - persona_model_config=persona_model_config, - agent_model_config=agent_model_config, - max_turns=4, - runs_per_prompt=1, - output_folder=str(tmp_path / "out"), - run_id="run1", - verbose=False, - ) - - kwargs = mock_runner_cls.call_args.kwargs - assert kwargs["persona_prompt_path"] == "data/SI/personas.tsv" - assert ( - kwargs["persona_context_template_path"] - == "data/SI/persona_context_template.txt" - ) + assert personas == [str(tmp_path / "personas_custom.tsv")] + assert context_template == str(tmp_path / "persona_context_custom.txt") @pytest.mark.asyncio -async def test_main_rubric_manifest_without_personas_raises_value_error( +async def test_resolve_persona_inputs_without_personas_raises_value_error( tmp_path: Path, ) -> None: """A manifest with no personas listed can't select a persona file.""" @@ -150,13 +128,4 @@ async def test_main_rubric_manifest_without_personas_raises_value_error( ) with pytest.raises(ValueError, match="no personas listed"): - await generate.main( - persona_model_config={"model": "mock-persona"}, - agent_model_config={"model": "mock-agent", "name": "mock-agent"}, - max_turns=4, - runs_per_prompt=1, - output_folder=str(tmp_path / "out"), - run_id="run1", - rubric_manifest=str(manifest_path), - verbose=False, - ) + await generate.resolve_persona_inputs(str(manifest_path)) From 873db2bacd98316f401d6f305f7430cfbed6d7c5 Mon Sep 17 00:00:00 2001 From: Luca Belli <129434630+sator-labs@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:20:29 -0700 Subject: [PATCH 2/4] docs: mark generate script as legacy --- generate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/generate.py b/generate.py index 0b247756c..1043cf48c 100644 --- a/generate.py +++ b/generate.py @@ -1,5 +1,8 @@ #!/usr/bin/env python3 +# Legacy CLI compatibility entry point. Remove this file once `vera generate` +# replaces direct `generate.py` usage; do not add new CLI behavior here. + import argparse import asyncio import sys From 2154c288e7444281ec2fdbee5a89523bcb7553dc Mon Sep 17 00:00:00 2001 From: Luca Belli <129434630+sator-labs@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:50:50 -0700 Subject: [PATCH 3/4] refactor: rename generation workflow module --- generate_conversations/__init__.py | 2 +- generate_conversations/{run.py => workflow.py} | 2 +- tests/unit/generate_conversations/test_generate_cli.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename generate_conversations/{run.py => workflow.py} (98%) diff --git a/generate_conversations/__init__.py b/generate_conversations/__init__.py index 83b2a1aab..d5c7a505c 100644 --- a/generate_conversations/__init__.py +++ b/generate_conversations/__init__.py @@ -1,6 +1,6 @@ """Generate Conversations Package - LLM Conversation Simulation""" -from .run import run_generation from .runner import ConversationRunner +from .workflow import run_generation __all__ = ["ConversationRunner", "run_generation"] diff --git a/generate_conversations/run.py b/generate_conversations/workflow.py similarity index 98% rename from generate_conversations/run.py rename to generate_conversations/workflow.py index c73461382..8371cd5c0 100644 --- a/generate_conversations/run.py +++ b/generate_conversations/workflow.py @@ -1,4 +1,4 @@ -"""Resolved-input application function for conversation generation.""" +"""Resolved-input workflow for conversation generation.""" from __future__ import annotations diff --git a/tests/unit/generate_conversations/test_generate_cli.py b/tests/unit/generate_conversations/test_generate_cli.py index 8fb488858..d71d00ec4 100644 --- a/tests/unit/generate_conversations/test_generate_cli.py +++ b/tests/unit/generate_conversations/test_generate_cli.py @@ -48,7 +48,7 @@ async def test_main_resume_uses_existing_run_folder(tmp_path: Path) -> None: run_folder = tmp_path / "p_mock_persona__a_mock_agent__t4__r1__20260331_120000" run_folder.mkdir(parents=True, exist_ok=True) - with patch("generate_conversations.run.ConversationRunner") as mock_runner_cls: + with patch("generate_conversations.workflow.ConversationRunner") as mock_runner_cls: mock_runner = mock_runner_cls.return_value mock_runner.run_conversations = AsyncMock(return_value=[]) From 4de14d7bddfb3bdbdd228eb1c9eaf00402ae342b Mon Sep 17 00:00:00 2001 From: Luca Belli <129434630+sator-labs@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:54:28 -0700 Subject: [PATCH 4/4] refactor: name generation application entry point --- generate_conversations/__init__.py | 2 +- generate_conversations/{workflow.py => main.py} | 7 ++++++- generate_conversations/runner.py | 7 +++++++ tests/unit/generate_conversations/test_generate_cli.py | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) rename generate_conversations/{workflow.py => main.py} (95%) diff --git a/generate_conversations/__init__.py b/generate_conversations/__init__.py index d5c7a505c..73b7f5f1d 100644 --- a/generate_conversations/__init__.py +++ b/generate_conversations/__init__.py @@ -1,6 +1,6 @@ """Generate Conversations Package - LLM Conversation Simulation""" +from .main import run_generation from .runner import ConversationRunner -from .workflow import run_generation __all__ = ["ConversationRunner", "run_generation"] diff --git a/generate_conversations/workflow.py b/generate_conversations/main.py similarity index 95% rename from generate_conversations/workflow.py rename to generate_conversations/main.py index 8371cd5c0..5561e40e2 100644 --- a/generate_conversations/workflow.py +++ b/generate_conversations/main.py @@ -1,4 +1,9 @@ -"""Resolved-input workflow for conversation generation.""" +"""Main application entry point for one resolved generation invocation. + +This module owns run-level setup and validation, then delegates conversation +execution to ``ConversationRunner``. CLI parsing, defaults, and target/config +resolution belong outside the generation domain. +""" from __future__ import annotations diff --git a/generate_conversations/runner.py b/generate_conversations/runner.py index d123320fe..052e93dd2 100644 --- a/generate_conversations/runner.py +++ b/generate_conversations/runner.py @@ -1,5 +1,12 @@ #!/usr/bin/env python3 +"""Execute the conversations in an already-configured generation run. + +``ConversationRunner`` loads the resolved personas, schedules persona/run jobs, +executes their sessions, and writes transcripts and logs. It does not define +the user-facing CLI contract or resolve config files and target manifests. +""" + import asyncio import logging import os diff --git a/tests/unit/generate_conversations/test_generate_cli.py b/tests/unit/generate_conversations/test_generate_cli.py index d71d00ec4..8c703f5a4 100644 --- a/tests/unit/generate_conversations/test_generate_cli.py +++ b/tests/unit/generate_conversations/test_generate_cli.py @@ -48,7 +48,7 @@ async def test_main_resume_uses_existing_run_folder(tmp_path: Path) -> None: run_folder = tmp_path / "p_mock_persona__a_mock_agent__t4__r1__20260331_120000" run_folder.mkdir(parents=True, exist_ok=True) - with patch("generate_conversations.workflow.ConversationRunner") as mock_runner_cls: + with patch("generate_conversations.main.ConversationRunner") as mock_runner_cls: mock_runner = mock_runner_cls.return_value mock_runner.run_conversations = AsyncMock(return_value=[])