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 across the May 2026 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 May 2026 released 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 "llama-cpp-python[server]" huggingface-hub
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 eight replicates per condition. To push to a larger sample size, raise the replicate count explicitly:

export PROMPT_STUDY_REPLICATES=12
make run-example

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 first run may therefore download a model before the walkthrough executes, which is why the setup above includes huggingface-hub.

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

When To Go Direct#

Use the umbrella package when you want one stable import surface for the ecosystem. Install a sibling package directly when you only need one layer or want package-specific internals. See Compatibility And Start Here for the tested version combination and install guidance.