podojo-cli 1.5.2__tar.gz → 1.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/CHANGELOG.md +5 -0
  2. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/PKG-INFO +7 -1
  3. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/README.md +6 -0
  4. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/pyproject.toml +1 -1
  5. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/src/podojo_cli/commands/aiinterviews.py +65 -1
  6. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/tests/test_aiinterviews.py +83 -0
  7. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/uv.lock +1 -1
  8. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/.github/workflows/publish.yml +0 -0
  9. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/.gitignore +0 -0
  10. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/CLAUDE.md +0 -0
  11. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/LICENSE +0 -0
  12. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/src/podojo_cli/__init__.py +0 -0
  13. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/src/podojo_cli/client.py +0 -0
  14. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/src/podojo_cli/commands/__init__.py +0 -0
  15. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/src/podojo_cli/commands/auth.py +0 -0
  16. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/src/podojo_cli/commands/interviews.py +0 -0
  17. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/src/podojo_cli/commands/projects.py +0 -0
  18. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/src/podojo_cli/commands/showreel.py +0 -0
  19. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/src/podojo_cli/commands/synth.py +0 -0
  20. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/src/podojo_cli/commands/transcripts.py +0 -0
  21. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/src/podojo_cli/commands/usertests.py +0 -0
  22. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/src/podojo_cli/commands/videos.py +0 -0
  23. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/src/podojo_cli/config.py +0 -0
  24. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/src/podojo_cli/main.py +0 -0
  25. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/src/podojo_cli/synth/__init__.py +0 -0
  26. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/src/podojo_cli/synth/driver.py +0 -0
  27. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/src/podojo_cli/synth/session.py +0 -0
  28. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/src/podojo_cli/version_check.py +0 -0
  29. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/src/podojo_cli/video/__init__.py +0 -0
  30. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/src/podojo_cli/video/showreel.py +0 -0
  31. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/tests/conftest.py +0 -0
  32. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/tests/test_auth.py +0 -0
  33. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/tests/test_interviews.py +0 -0
  34. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/tests/test_projects.py +0 -0
  35. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/tests/test_showreel.py +0 -0
  36. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/tests/test_transcripts.py +0 -0
  37. {podojo_cli-1.5.2 → podojo_cli-1.6.0}/tests/test_usertests.py +0 -0
@@ -5,6 +5,11 @@ All notable changes to the Podojo CLI will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org).
7
7
 
8
+ ## [1.6.0] - 2026-07-03
9
+
10
+ ### Added
11
+ - AI interviews support an optional participant screener: `screening_questions` (single-select, options flagged `qualifies: true`) and `rejection_message`. `validate`/`create` check the screener shape, and `example` shows the new fields.
12
+
8
13
  ## [1.5.2] - 2026-07-02
9
14
 
10
15
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: podojo-cli
3
- Version: 1.5.2
3
+ Version: 1.6.0
4
4
  Summary: CLI for the Podojo user research platform
5
5
  Project-URL: Homepage, https://github.com/podojo/cli-podojo
6
6
  Project-URL: Source, https://github.com/podojo/cli-podojo
@@ -143,6 +143,12 @@ podojo aiinterviews delete checkout-experience-v1
143
143
  base URL can be overridden via `ai_interviews_url` in `~/.podojo.toml` or
144
144
  `PODOJO_AI_INTERVIEWS_URL`.
145
145
 
146
+ A study can open with an optional participant screener: on-screen single-select
147
+ `screening_questions` whose options carry `qualifies: true` flags. Participants
148
+ must pick a qualifying option on every question; everyone else sees the study's
149
+ `rejection_message` and never reaches the voice interview. See
150
+ `podojo aiinterviews example` for the exact shape.
151
+
146
152
  ### Synthetic participants
147
153
 
148
154
  The `synth` group drives a Playwright browser through a user test preview so an
@@ -112,6 +112,12 @@ podojo aiinterviews delete checkout-experience-v1
112
112
  base URL can be overridden via `ai_interviews_url` in `~/.podojo.toml` or
113
113
  `PODOJO_AI_INTERVIEWS_URL`.
114
114
 
115
+ A study can open with an optional participant screener: on-screen single-select
116
+ `screening_questions` whose options carry `qualifies: true` flags. Participants
117
+ must pick a qualifying option on every question; everyone else sees the study's
118
+ `rejection_message` and never reaches the voice interview. See
119
+ `podojo aiinterviews example` for the exact shape.
120
+
115
121
  ### Synthetic participants
116
122
 
117
123
  The `synth` group drives a Playwright browser through a user test preview so an
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "podojo-cli"
3
- version = "1.5.2"
3
+ version = "1.6.0"
4
4
  description = "CLI for the Podojo user research platform"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -24,13 +24,21 @@ EXAMPLE_YAML = """\
24
24
  #
25
25
  # Required fields: interview_id, title, questions, closing_message
26
26
  # Optional fields: language (default en-US), project_name, overview,
27
- # decision, live
27
+ # decision, screening_questions, rejection_message, live
28
28
  #
29
29
  # Each question:
30
30
  # text (required) the main question, asked verbatim-ish in order
31
31
  # section (optional) researcher-facing grouping label
32
32
  # max_follow_ups (optional, default 2) adaptive follow-up budget
33
33
  # probe_for (optional) what a concrete answer must cover — drives follow-ups
34
+ #
35
+ # Each screening question (optional participant screener, answered on screen
36
+ # before the voice interview starts):
37
+ # text (required) single-select multiple-choice question
38
+ # options (required, at least 2) each with `text` and an optional
39
+ # `qualifies: true` — participants must pick a qualifying
40
+ # option on every question, otherwise they see the
41
+ # rejection_message and the interview never starts
34
42
 
35
43
  interview_id: checkout-experience-v1
36
44
  title: Checkout Experience Research
@@ -57,6 +65,30 @@ decision: >
57
65
  # Optional: set interview live (default: false)
58
66
  # live: true
59
67
 
68
+ # Optional: participant screener — shown on screen (no audio) before the
69
+ # conversation. Answers are captured alongside the session's recording.
70
+ screening_questions:
71
+ - text: How often do you shop online?
72
+ options:
73
+ - text: Rarely or never
74
+ - text: A few times a year
75
+ - text: At least once a month
76
+ qualifies: true
77
+ - text: Weekly or more
78
+ qualifies: true
79
+
80
+ - text: Have you abandoned an online purchase at checkout in the past 3 months?
81
+ options:
82
+ - text: "Yes"
83
+ qualifies: true
84
+ - text: "No"
85
+ - text: Not sure
86
+
87
+ # Optional: shown to participants whose screener answers don't qualify
88
+ rejection_message: >
89
+ Thank you for your time, you did not meet the research criteria for this
90
+ study!
91
+
60
92
  questions:
61
93
  - section: Shopping Habits
62
94
  text: >
@@ -118,6 +150,38 @@ def validate_ai_interview_data(data: dict) -> list[str]:
118
150
  errors.append(
119
151
  f"Question {i}: 'max_follow_ups' must be an integer >= 0, got '{max_follow_ups}'"
120
152
  )
153
+
154
+ screening_questions = data.get("screening_questions")
155
+ if screening_questions is not None:
156
+ if not isinstance(screening_questions, list):
157
+ errors.append("'screening_questions' must be a list")
158
+ else:
159
+ for i, question in enumerate(screening_questions, 1):
160
+ if not isinstance(question, dict) or "text" not in question:
161
+ errors.append(f"Screening question {i}: must be a mapping with 'text'")
162
+ continue
163
+ options = question.get("options")
164
+ if not isinstance(options, list) or len(options) < 2:
165
+ errors.append(f"Screening question {i}: 'options' must list at least 2 options")
166
+ continue
167
+ qualifying = 0
168
+ for j, option in enumerate(options, 1):
169
+ if not isinstance(option, dict) or "text" not in option:
170
+ errors.append(
171
+ f"Screening question {i}, option {j}: must be a mapping with 'text'"
172
+ )
173
+ continue
174
+ qualifies = option.get("qualifies", False)
175
+ if not isinstance(qualifies, bool):
176
+ errors.append(
177
+ f"Screening question {i}, option {j}: 'qualifies' must be true or false"
178
+ )
179
+ elif qualifies:
180
+ qualifying += 1
181
+ if qualifying == 0:
182
+ errors.append(
183
+ f"Screening question {i}: needs at least one option with 'qualifies: true'"
184
+ )
121
185
  return errors
122
186
 
123
187
 
@@ -305,6 +305,87 @@ def test_validate_invalid(runner, tmp_path):
305
305
  assert "Missing required field" in result.output
306
306
 
307
307
 
308
+ SCREENING_YAML = VALID_AI_INTERVIEW_YAML + """\
309
+ screening_questions:
310
+ - text: Do you currently drive for a ridehailing platform?
311
+ options:
312
+ - text: "Yes"
313
+ qualifies: true
314
+ - text: "No"
315
+ - text: How many rides did you complete last month?
316
+ options:
317
+ - text: 0 rides
318
+ - text: 1-10 rides
319
+ qualifies: true
320
+ - text: More than 10 rides
321
+ qualifies: true
322
+ rejection_message: You did not meet the research criteria for this study.
323
+ """
324
+
325
+
326
+ def test_validate_with_screening(runner, tmp_path):
327
+ yaml_file = tmp_path / "interview.yaml"
328
+ yaml_file.write_text(SCREENING_YAML)
329
+
330
+ result = runner.invoke(app, ["aiinterviews", "validate", str(yaml_file)])
331
+
332
+ assert result.exit_code == 0
333
+ assert "Valid AI interview config" in result.output
334
+
335
+
336
+ def test_validate_screening_question_needs_two_options(runner, tmp_path):
337
+ yaml_file = tmp_path / "interview.yaml"
338
+ yaml_file.write_text(
339
+ VALID_AI_INTERVIEW_YAML
340
+ + "screening_questions:\n"
341
+ + " - text: Only one way to answer?\n"
342
+ + " options:\n"
343
+ + " - text: \"Yes\"\n"
344
+ + " qualifies: true\n"
345
+ )
346
+
347
+ result = runner.invoke(app, ["aiinterviews", "validate", str(yaml_file)])
348
+
349
+ assert result.exit_code == 1
350
+ assert "'options' must list at least 2 options" in result.output
351
+
352
+
353
+ def test_validate_screening_question_needs_a_qualifying_option(runner, tmp_path):
354
+ yaml_file = tmp_path / "interview.yaml"
355
+ yaml_file.write_text(
356
+ VALID_AI_INTERVIEW_YAML
357
+ + "screening_questions:\n"
358
+ + " - text: Nobody can pass this one\n"
359
+ + " options:\n"
360
+ + " - text: A\n"
361
+ + " - text: B\n"
362
+ )
363
+
364
+ result = runner.invoke(app, ["aiinterviews", "validate", str(yaml_file)])
365
+
366
+ assert result.exit_code == 1
367
+ assert "needs at least one option with 'qualifies: true'" in result.output
368
+
369
+
370
+ def test_validate_screening_option_qualifies_must_be_bool(runner, tmp_path):
371
+ yaml_file = tmp_path / "interview.yaml"
372
+ yaml_file.write_text(
373
+ VALID_AI_INTERVIEW_YAML
374
+ + "screening_questions:\n"
375
+ + " - text: Q\n"
376
+ + " options:\n"
377
+ + " - text: A\n"
378
+ + " qualifies: yep\n"
379
+ + " - text: B\n"
380
+ + " qualifies: true\n"
381
+ )
382
+
383
+ result = runner.invoke(app, ["aiinterviews", "validate", str(yaml_file)])
384
+
385
+ assert result.exit_code == 1
386
+ assert "'qualifies' must be true or false" in result.output
387
+
388
+
308
389
  def test_example(runner):
309
390
  result = runner.invoke(app, ["aiinterviews", "example"])
310
391
 
@@ -313,6 +394,8 @@ def test_example(runner):
313
394
  assert "questions:" in result.output
314
395
  assert "closing_message:" in result.output
315
396
  assert "max_follow_ups:" in result.output
397
+ assert "screening_questions:" in result.output
398
+ assert "rejection_message:" in result.output
316
399
 
317
400
 
318
401
  def test_example_validates(runner, tmp_path):
@@ -369,7 +369,7 @@ wheels = [
369
369
 
370
370
  [[package]]
371
371
  name = "podojo-cli"
372
- version = "1.5.1"
372
+ version = "1.6.0"
373
373
  source = { editable = "." }
374
374
  dependencies = [
375
375
  { name = "httpx" },
File without changes
File without changes
File without changes
File without changes