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.
What This Covers#
resolves a real packaged problem through
design_research.problemsbuilds the study from
design_research.experiments.build_strategy_comparison_studywith a recipe-first benchmark bundle containing a random baseline, a neutral prompt, and a profit-focused promptruns the live study through
design_research.experiments.run_studyexports the canonical study artifacts plus a markdown summary report built from
render_markdown_summary,render_methods_scaffold,render_codebook, andrender_significance_briefvalidates the exported event rows through
design_research.analysiscomputes ordered one-sided condition-pair permutation tests from the exported
runs.csvandevaluations.csvtables viabuild_condition_metric_tableandcompare_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.