Prompt Strategy Comparison Walkthrough#

This walkthrough demonstrates the umbrella package doing real work with a live model-backed agent while following the comparison-study recipe/reporting APIs published in the pinned sibling-library releases. It uses a real packaged problem from design_research.problems, a managed prompt-mode design_research.agents.Workflow, design_research.agents.PromptWorkflowAgent, the design_research.experiments.build_strategy_comparison_study scaffold, and the newer condition-comparison helpers from design_research.analysis.

Flow diagram showing a packaged problem feeding a live workflow agent, then study execution, artifact export, and event-table validation.

What This Covers#

  • resolves a real packaged problem through design_research.problems

  • builds the study from design_research.experiments.build_strategy_comparison_study with a recipe-first benchmark bundle containing a random baseline, a neutral prompt, and a profit-focused prompt

  • runs the live study through design_research.experiments.run_study

  • exports the canonical study artifacts plus a markdown summary report built from render_markdown_summary, render_methods_scaffold, render_codebook, and render_significance_brief

  • validates the exported event rows through design_research.analysis

  • computes ordered one-sided condition-pair permutation tests from the exported runs.csv and evaluations.csv tables via build_condition_metric_table and compare_condition_pairs

Package Alignment#

This local walkthrough intentionally tracks the pinned package APIs from design-research-agents, design-research-experiments, and design-research-analysis. If you run it against older releases of those sibling packages, it will fail fast with a clear upgrade message instead of silently drifting from the new workflow/recipe/reporting surface.

During local development, the umbrella test harness can point subprocess runs at adjacent sibling worktrees so the examples stay validated against the same public APIs owned by the sibling libraries themselves.

Run It#

python -m pip install "design-research-agents[llama_cpp]==0.6.0"
make run-example

Optionally point the walkthrough at a specific local GGUF file:

export LLAMA_CPP_MODEL=/path/to/model.gguf
make run-example

The default configuration uses 50 replicates per condition. To choose a different sample size, set the replicate count explicitly:

export PROMPT_STUDY_REPLICATES=12
make run-example

Maintainers can use make live-smoke-llama-cpp for a smoke-sized semantic check with two replicates per condition. The smoke target still exercises both model-backed prompt strategies; it does not replace the full walkthrough when you need study-scale results.

The example writes canonical exports to artifacts/examples/prompt_strategy_comparison_study and writes a markdown summary report to artifacts/examples/prompt_strategy_comparison_study/artifacts/prompt_strategy_summary.md. It prints condition means, a condition-comparison brief, a significance brief, the summary-report path, exported artifact paths, and the event-table validation summary. The script intentionally has no deterministic fallback path for the live-agent conditions: it expects a real llama.cpp runtime.

If LLAMA_CPP_MODEL is not set, the client falls back to its built-in model defaults and Hugging Face repo settings. The Agents llama_cpp extra includes that download client, so the first run may download a model before the walkthrough executes. The managed client allows up to five minutes for that first startup. On an unusually slow connection, set LLAMA_CPP_STARTUP_TIMEOUT_SECONDS to a larger number of seconds.

The script is intentionally written in a linear, step-by-step style so it can double as training material and as the literal-included documentation example. The only local callbacks left in place are the small workflow request/response adapters and the condition-specific prompt builders passed into PromptWorkflowAgent(...).

Code#

examples/prompt_framing_study.py#
  1"""Canonical live strategy-comparison walkthrough for the umbrella package."""
  2
  3from __future__ import annotations
  4
  5import importlib.util
  6import os
  7from collections.abc import Sequence
  8from pathlib import Path
  9
 10import design_research as dr
 11
 12# These constants keep the live walkthrough readable: one packaged problem, one
 13# study id, stable artifact paths, and the statistical settings used in the
 14# pairwise comparisons later on.
 15BASELINE_AGENT_ID = "SeededRandomBaselineAgent"
 16PROBLEM_ID = "decision_laptop_design_profit_maximization"
 17STUDY_ID = "prompt_strategy_comparison_study"
 18OUTPUT_DIR = Path("artifacts") / "examples" / STUDY_ID
 19SUMMARY_REPORT_NAME = "prompt_strategy_summary.md"
 20DEFAULT_REPLICATES_PER_CONDITION = 50
 21SIGNIFICANCE_ALPHA = 0.05
 22EXACT_PERMUTATION_THRESHOLD = 250_000
 23MONTE_CARLO_PERMUTATIONS = 20_000
 24PERMUTATION_TEST_SEED = 17
 25STRATEGY_ORDER = (BASELINE_AGENT_ID, "neutral_prompt", "profit_focus_prompt")
 26MODEL_BACKED_STRATEGY_IDS = ("neutral_prompt", "profit_focus_prompt")
 27PRIMARY_METRIC = "predicted_share"
 28SECONDARY_METRIC = "expected_demand_units"
 29PAIRWISE_COMPARISONS = (
 30    ("profit_focus_prompt", "neutral_prompt"),
 31    ("neutral_prompt", BASELINE_AGENT_ID),
 32    ("profit_focus_prompt", BASELINE_AGENT_ID),
 33)
 34
 35
 36def main() -> None:
 37    """Run the live strategy-comparison walkthrough with managed llama.cpp."""
 38    # Read runtime settings from the environment and apply the example's default
 39    # replicate count when the user does not override it.
 40    runtime = llama_cpp_runtime_config(default_replicates=DEFAULT_REPLICATES_PER_CONDITION)
 41
 42    # Load the packaged decision problem and derive the JSON candidate schema the
 43    # model-based agents should emit.
 44    packaged_problem = dr.problems.get_problem(PROBLEM_ID)
 45    candidate_schema = decision_candidate_schema(packaged_problem)
 46
 47    # Build the recipe-defined study and then materialize its conditions. The
 48    # conditions encode one row per strategy/replicate combination.
 49    study = _build_study(replicates=int(runtime["replicates"]))
 50    conditions = dr.experiments.build_design(study)
 51
 52    # Start a managed llama.cpp server client for the duration of the study.
 53    # The context manager handles startup/shutdown around the live run.
 54    with dr.agents.LlamaCppServerLLMClient(
 55        model=str(runtime["model_source"]),
 56        hf_model_repo_id=runtime["model_repo"],
 57        api_model=str(runtime["model_name"]),
 58        host=str(runtime["host"]),
 59        port=int(runtime["port"]),
 60        context_window=int(runtime["context_window"]),
 61        startup_timeout_seconds=float(runtime["startup_timeout_seconds"]),
 62        request_timeout_seconds=float(runtime["request_timeout_seconds"]),
 63    ) as llm_client:
 64        # Each `agent_id` in the strategy bundle maps either to a public agent
 65        # id resolved directly by experiments or to one explicit binding that
 66        # returns a prompt-driven workflow agent.
 67        agent_bindings = {
 68            # The neutral condition uses the live model but keeps the instruction
 69            # framing generic.
 70            "neutral_prompt": _prompt_agent_binding(
 71                llm_client=llm_client,
 72                candidate_schema=candidate_schema,
 73                runtime=runtime,
 74                instruction=(
 75                    "Condition: neutral prompt. Choose the best overall candidate using the "
 76                    "packaged demand and feasibility information."
 77                ),
 78            ),
 79            # The profit-focused condition swaps only the framing instruction so
 80            # the study isolates prompt strategy rather than model identity.
 81            "profit_focus_prompt": _prompt_agent_binding(
 82                llm_client=llm_client,
 83                candidate_schema=candidate_schema,
 84                runtime=runtime,
 85                instruction=(
 86                    "Condition: profit-focus prompt. Prioritize choices that maximize "
 87                    "market share proxy and expected demand."
 88                ),
 89            ),
 90        }
 91
 92        # Execute the full study while the managed llama.cpp client is running.
 93        results = dr.experiments.run_study(
 94            study,
 95            conditions=conditions,
 96            agent_bindings=agent_bindings,
 97            checkpoint=False,
 98            show_progress=False,
 99        )
100
101    # Treat this as a live-runtime check, not merely a deterministic-baseline
102    # check: every model-backed prompt strategy must produce usable evidence.
103    successful_results = _require_successful_model_strategies(results)
104
105    # Export the standard analysis tables so the next steps can work from the
106    # same artifacts users would inspect after their own runs.
107    artifact_paths = dr.experiments.export_analysis_tables(
108        study,
109        conditions=conditions,
110        run_results=results,
111        output_dir=OUTPUT_DIR,
112    )
113
114    # Confirm that the event-level export is structurally valid before building
115    # downstream tables from it.
116    validation_report = dr.analysis.validate_experiment_events(artifact_paths["events.csv"])
117
118    # Build one condition-by-metric table for the primary outcome we care about
119    # and another for a secondary business-facing metric, without hand-loading CSVs.
120    primary_metric_rows = dr.analysis.build_condition_metric_table_from_artifacts(
121        artifact_paths["events.csv"],
122        metric=PRIMARY_METRIC,
123        condition_column="agent_id",
124    )
125    demand_metric_rows = dr.analysis.build_condition_metric_table_from_artifacts(
126        artifact_paths["events.csv"],
127        metric=SECONDARY_METRIC,
128        condition_column="agent_id",
129    )
130
131    # Compare the strategy pairs using the analysis package's pairwise
132    # permutation test helper.
133    comparison_report = dr.analysis.compare_condition_pairs_from_artifacts(
134        artifact_paths["events.csv"],
135        metric=PRIMARY_METRIC,
136        condition_column="agent_id",
137        condition_pairs=PAIRWISE_COMPARISONS,
138        alternative="greater",
139        alpha=SIGNIFICANCE_ALPHA,
140        exact_threshold=EXACT_PERMUTATION_THRESHOLD,
141        n_permutations=MONTE_CARLO_PERMUTATIONS,
142        seed=PERMUTATION_TEST_SEED,
143    )
144
145    # Convert the statistical report into rows that the experiments reporting
146    # helpers can render alongside the study summary.
147    significance_rows = comparison_report.to_significance_rows()
148
149    # Write one consolidated markdown report that includes the study summary,
150    # methods scaffold, variable codebook, and the pairwise comparison brief.
151    summary_path = dr.experiments.write_markdown_report(
152        study.output_dir,
153        SUMMARY_REPORT_NAME,
154        "\n\n".join(
155            [
156                dr.experiments.render_markdown_summary(study, results),
157                dr.experiments.render_methods_scaffold(study),
158                dr.experiments.render_codebook(study, conditions),
159                comparison_report.render_brief(),
160                dr.experiments.render_significance_brief(significance_rows),
161            ]
162        ),
163    )
164
165    # Collapse the metric tables to per-strategy means for a concise console
166    # summary after the run finishes.
167    primary_means = condition_means(primary_metric_rows)
168    demand_means = condition_means(demand_metric_rows)
169
170    # Fail loudly if the exported live data is not structurally usable.
171    if validation_report.errors:
172        raise RuntimeError(
173            "Unified event table validation failed:\n- " + "\n- ".join(validation_report.errors)
174        )
175
176    # Print a guided end-of-run summary so the console output doubles as a quick
177    # tour of the artifacts and the headline comparison result.
178    print("Problem:", PROBLEM_ID)
179    print("Study:", study.study_id)
180    print("Live provider:", runtime["provider_name"])
181    print("Live model API name:", runtime["model_name"])
182    print("Model source:", runtime["model_source"])
183    print("Replicates per condition:", runtime["replicates"])
184    print("Conditions:", len(conditions))
185    print("Runs:", len(results), f"({len(successful_results)} success)")
186    print("Condition means:")
187    for strategy_name in STRATEGY_ORDER:
188        print(
189            f"  - agent_id={strategy_name}: "
190            f"mean_{PRIMARY_METRIC}={primary_means.get(strategy_name, 0.0):.4f}, "
191            f"mean_{SECONDARY_METRIC}={demand_means.get(strategy_name, 0.0):.0f}"
192        )
193    print(comparison_report.render_brief())
194    print(dr.experiments.render_significance_brief(significance_rows))
195    print("Event rows valid:", validation_report.is_valid, f"(rows={validation_report.n_rows})")
196    print("Summary report:", summary_path)
197    print("Artifacts:", artifact_names(artifact_paths))
198
199
200def _build_study(*, replicates: int) -> object:
201    """Build the live strategy-comparison recipe study."""
202    # The recipe builder captures the study in one config object. The bundle says
203    # which packaged problems and agent strategies participate; the run budget
204    # says how many replicates to execute.
205    return dr.experiments.build_strategy_comparison_study(
206        dr.experiments.StrategyComparisonConfig(
207            study_id=STUDY_ID,
208            title="Prompt Strategy Comparison Study",
209            description=(
210                "Compare a seeded random baseline, a neutral prompt, and a profit-focused "
211                "prompt on a packaged laptop-design decision problem."
212            ),
213            bundle=dr.experiments.BenchmarkBundle(
214                bundle_id="live-strategy-comparison",
215                name="Live Strategy Comparison Bundle",
216                description="Packaged decision problem with three strategy bindings.",
217                problem_ids=(PROBLEM_ID,),
218                agent_specs=STRATEGY_ORDER,
219            ),
220            run_budget=dr.experiments.RunBudget(replicates=replicates, parallelism=1),
221            output_dir=OUTPUT_DIR,
222        )
223    )
224
225
226def _strategy_prompt(problem_packet: object, *, instruction: str) -> str:
227    """Render one complete strategy prompt from the normalized problem packet."""
228    # Compose the final prompt from a few readable pieces instead of one giant
229    # literal string. That makes it easy to see which lines stay fixed across
230    # conditions and which line changes with the strategy framing.
231    return "\n".join(
232        [
233            "You are solving a packaged design-research decision problem.",
234            "Read the problem brief and return exactly one JSON object candidate.",
235            instruction,
236            "",
237            str(getattr(problem_packet, "brief", "")).strip(),
238            "",
239            "Return JSON only with no markdown fences and no extra commentary.",
240        ]
241    )
242
243
244def artifact_names(artifact_paths: dict[str, Path]) -> str:
245    """Return exported artifact filenames in stable sorted order."""
246    return ", ".join(sorted(path.name for path in artifact_paths.values()))
247
248
249def condition_means(rows: list[dict[str, object]]) -> dict[str, float]:
250    """Compute one mean per condition label from normalized rows."""
251    grouped: dict[str, list[float]] = {}
252    for row in rows:
253        grouped.setdefault(str(row["condition"]), []).append(float(row["value"]))
254    return {
255        condition: (sum(values) / len(values) if values else 0.0)
256        for condition, values in grouped.items()
257    }
258
259
260def _require_successful_model_strategies(results: Sequence[object]) -> list[object]:
261    """Require observed successes from every model-backed prompt strategy."""
262    successful_results: list[object] = []
263    successful_strategy_ids: set[str] = set()
264    model_attempts: dict[str, list[str]] = {
265        strategy_id: [] for strategy_id in MODEL_BACKED_STRATEGY_IDS
266    }
267    for result in results:
268        raw_status = getattr(result, "status", None)
269        status = getattr(raw_status, "value", raw_status)
270        run_spec = getattr(result, "run_spec", None)
271        strategy_ref = getattr(run_spec, "agent_spec_ref", None)
272        strategy_id = str(strategy_ref) if strategy_ref is not None else None
273        if strategy_id in model_attempts:
274            error_info = getattr(result, "error_info", None)
275            detail = str(error_info).strip() if error_info else str(status)
276            if detail not in model_attempts[strategy_id]:
277                model_attempts[strategy_id].append(detail)
278        if status != "success":
279            continue
280        successful_results.append(result)
281        if strategy_id is not None:
282            successful_strategy_ids.add(strategy_id)
283
284    missing_strategy_ids = [
285        strategy_id
286        for strategy_id in MODEL_BACKED_STRATEGY_IDS
287        if strategy_id not in successful_strategy_ids
288    ]
289    if missing_strategy_ids:
290        attempt_summary = "; ".join(
291            f"{strategy_id}: {', '.join(model_attempts[strategy_id]) or 'no result'}"
292            for strategy_id in missing_strategy_ids
293        )
294        raise RuntimeError(
295            "The live walkthrough requires at least one successful result from each "
296            "model-backed prompt strategy. Missing successful strategies: "
297            + ", ".join(missing_strategy_ids)
298            + ". Observed attempts: "
299            + attempt_summary
300        )
301    return successful_results
302
303
304def decision_candidate_schema(problem: object) -> dict[str, object]:
305    """Build a JSON schema for discrete decision-factor candidates."""
306    properties: dict[str, object] = {}
307    required: list[str] = []
308    for factor in getattr(problem, "option_factors", ()):
309        levels = tuple(getattr(factor, "levels", ()))
310        key = str(getattr(factor, "key", ""))
311        if not key or not levels:
312            continue
313        properties[key] = {"type": "number", "enum": list(levels)}
314        required.append(key)
315
316    if not required:
317        raise RuntimeError("Expected a packaged decision problem with explicit option factors.")
318
319    return {
320        "type": "object",
321        "properties": properties,
322        "required": required,
323        "additionalProperties": False,
324    }
325
326
327def llama_cpp_runtime_config(*, default_replicates: int) -> dict[str, object]:
328    """Resolve runtime configuration and fail fast on missing live dependencies."""
329    missing_runtime = [
330        module_name
331        for module_name in ("llama_cpp", "fastapi", "uvicorn")
332        if importlib.util.find_spec(module_name) is None
333    ]
334    if missing_runtime:
335        raise RuntimeError(
336            "Install the owning Agents extra before running the live walkthrough: "
337            'python -m pip install "design-research-agents[llama_cpp]==0.6.0". Missing: '
338            + ", ".join(sorted(missing_runtime))
339        )
340
341    model_source = (
342        os.getenv("LLAMA_CPP_MODEL", "Qwen2.5-1.5B-Instruct-Q4_K_M.gguf").strip()
343        or "Qwen2.5-1.5B-Instruct-Q4_K_M.gguf"
344    )
345    model_repo = (
346        os.getenv("LLAMA_CPP_HF_MODEL_REPO_ID", "bartowski/Qwen2.5-1.5B-Instruct-GGUF").strip()
347        or None
348    )
349    if (
350        model_repo
351        and not Path(model_source).expanduser().exists()
352        and importlib.util.find_spec("huggingface_hub") is None
353    ):
354        raise RuntimeError(
355            "Install the owning Agents extra with "
356            'python -m pip install "design-research-agents[llama_cpp]==0.6.0" or point '
357            "LLAMA_CPP_MODEL at a local GGUF file before running the live walkthrough."
358        )
359
360    replicates = int(os.getenv("PROMPT_STUDY_REPLICATES", str(default_replicates)))
361    if replicates < 2:
362        raise RuntimeError("PROMPT_STUDY_REPLICATES must be at least 2.")
363
364    return {
365        "provider_name": "llama-cpp",
366        "model_source": model_source,
367        "model_name": os.getenv("LLAMA_CPP_API_MODEL", "qwen2-1.5b-q4").strip() or "qwen2-1.5b-q4",
368        "model_repo": model_repo,
369        "host": os.getenv("LLAMA_CPP_HOST", "127.0.0.1").strip() or "127.0.0.1",
370        "port": int(os.getenv("LLAMA_CPP_PORT", "8001")),
371        "context_window": int(os.getenv("LLAMA_CPP_CONTEXT_WINDOW", "4096")),
372        # The first startup may include a roughly 1 GB model download. Keep the
373        # wait finite but long enough for an ordinary laptop connection.
374        "startup_timeout_seconds": float(os.getenv("LLAMA_CPP_STARTUP_TIMEOUT_SECONDS", "300")),
375        "request_timeout_seconds": float(os.getenv("LLAMA_CPP_REQUEST_TIMEOUT_SECONDS", "120")),
376        "replicates": replicates,
377    }
378
379
380def _prompt_agent_binding(
381    *,
382    llm_client: object,
383    candidate_schema: dict[str, object],
384    runtime: dict[str, object],
385    instruction: str,
386) -> object:
387    """Build one condition-scoped prompt workflow agent binding."""
388
389    def _binding(_condition: object) -> object:
390        """Return one prompt workflow agent for a concrete experiment condition."""
391        return dr.agents.PromptWorkflowAgent(
392            workflow=dr.agents.build_json_prompt_workflow(
393                llm_client=llm_client,
394                response_schema=candidate_schema,
395                request_metadata={"study_id": STUDY_ID, "problem_id": PROBLEM_ID},
396                default_request_id_prefix=STUDY_ID,
397                fallback_model_name=str(runtime["model_name"]),
398                fallback_provider=str(runtime["provider_name"]),
399            ),
400            prompt_builder=lambda problem_packet, _run_spec, _condition: _strategy_prompt(
401                problem_packet,
402                instruction=instruction,
403            ),
404        )
405
406    return _binding
407
408
409if __name__ == "__main__":
410    main()

When To Go Direct#

Use the umbrella package when you want one import route for the exact tested package family. Install a component directly when you only need one package or want package-specific internals and extras. See Compatibility And Package Status for the tested version combination and API scope.