nardial 0.3.3__tar.gz → 0.4.2__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 (91) hide show
  1. {nardial-0.3.3 → nardial-0.4.2}/PKG-INFO +300 -33
  2. {nardial-0.3.3 → nardial-0.4.2}/README.md +297 -31
  3. {nardial-0.3.3 → nardial-0.4.2}/pyproject.toml +4 -2
  4. nardial-0.4.2/src/nardial/agenda/__init__.py +0 -0
  5. nardial-0.4.2/src/nardial/agenda/items.py +239 -0
  6. nardial-0.4.2/src/nardial/agenda/resolver.py +59 -0
  7. nardial-0.4.2/src/nardial/agenda/session_plan.py +95 -0
  8. nardial-0.4.2/src/nardial/agenda/slot_bounds.py +48 -0
  9. nardial-0.4.2/src/nardial/authoring/__init__.py +1 -0
  10. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/authoring/factory.py +146 -116
  11. nardial-0.4.2/src/nardial/authoring/loader.py +138 -0
  12. nardial-0.4.2/src/nardial/controls.py +184 -0
  13. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/conversation_agent.py +16 -6
  14. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/conversation_state.py +91 -0
  15. nardial-0.4.2/src/nardial/dialog_registry.py +63 -0
  16. nardial-0.4.2/src/nardial/dialog_types.py +50 -0
  17. nardial-0.4.2/src/nardial/eligibility.py +205 -0
  18. nardial-0.4.2/src/nardial/events/sources.py +68 -0
  19. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/interaction_orchestrator.py +89 -4
  20. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/mini_dialogs.py +468 -35
  21. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/moves.py +169 -7
  22. nardial-0.4.2/src/nardial/providers/device/reachy_mini.py +52 -0
  23. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/nlu/__init__.py +2 -0
  24. nardial-0.4.2/src/nardial/providers/nlu/dialogflow.py +65 -0
  25. nardial-0.4.2/src/nardial/providers/nlu/whisper.py +69 -0
  26. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/screen/web/static/screen.js +11 -1
  27. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/tts/elevenlabs.py +1 -1
  28. nardial-0.4.2/src/nardial/session_manager.py +378 -0
  29. {nardial-0.3.3 → nardial-0.4.2}/src/nardial.egg-info/PKG-INFO +300 -33
  30. {nardial-0.3.3 → nardial-0.4.2}/src/nardial.egg-info/SOURCES.txt +24 -2
  31. {nardial-0.3.3 → nardial-0.4.2}/src/nardial.egg-info/requires.txt +1 -0
  32. nardial-0.4.2/tests/test_agenda_items.py +327 -0
  33. nardial-0.4.2/tests/test_agenda_resolver.py +73 -0
  34. {nardial-0.3.3 → nardial-0.4.2}/tests/test_authoring_factory.py +46 -0
  35. nardial-0.4.2/tests/test_controls.py +319 -0
  36. {nardial-0.3.3 → nardial-0.4.2}/tests/test_conversation_agent.py +20 -0
  37. nardial-0.4.2/tests/test_conversation_state.py +156 -0
  38. nardial-0.4.2/tests/test_dialog_registry.py +89 -0
  39. nardial-0.4.2/tests/test_dialog_types.py +150 -0
  40. nardial-0.4.2/tests/test_eligibility_rules.py +240 -0
  41. nardial-0.4.2/tests/test_listen_timeout.py +171 -0
  42. {nardial-0.3.3 → nardial-0.4.2}/tests/test_moves.py +17 -1
  43. nardial-0.4.2/tests/test_session_manager.py +434 -0
  44. nardial-0.4.2/tests/test_session_plan.py +113 -0
  45. nardial-0.4.2/tests/test_slot_bounds.py +58 -0
  46. nardial-0.4.2/tests/test_type_definitions.py +302 -0
  47. nardial-0.3.3/src/nardial/authoring/__init__.py +0 -1
  48. nardial-0.3.3/src/nardial/authoring/loader.py +0 -70
  49. nardial-0.3.3/src/nardial/dialog_logic.py +0 -391
  50. nardial-0.3.3/src/nardial/providers/nlu/dialogflow.py +0 -38
  51. nardial-0.3.3/src/nardial/session_manager.py +0 -219
  52. nardial-0.3.3/tests/test_conversation_state.py +0 -57
  53. {nardial-0.3.3 → nardial-0.4.2}/LICENSE +0 -0
  54. {nardial-0.3.3 → nardial-0.4.2}/setup.cfg +0 -0
  55. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/__init__.py +0 -0
  56. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/authoring/export_html.py +0 -0
  57. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/events/__init__.py +0 -0
  58. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/events/bus.py +0 -0
  59. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/events/types.py +0 -0
  60. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/__init__.py +0 -0
  61. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/device/__init__.py +0 -0
  62. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/device/alphamini.py +0 -0
  63. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/device/desktop.py +0 -0
  64. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/device/nao.py +0 -0
  65. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/device/pepper.py +0 -0
  66. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/llm/__init__.py +0 -0
  67. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/llm/echo.py +0 -0
  68. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/llm/openai_gpt.py +0 -0
  69. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/nlu/written_keyword.py +0 -0
  70. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/screen/__init__.py +0 -0
  71. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/screen/null.py +0 -0
  72. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/screen/pepper_tablet.py +0 -0
  73. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/screen/sic_adapter.py +0 -0
  74. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/screen/web/static/screen.css +0 -0
  75. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/screen/web/static/socket.io.min.js +0 -0
  76. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/screen/web/templates/index.html +0 -0
  77. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/tts/__init__.py +0 -0
  78. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/tts/cacher.py +0 -0
  79. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/tts/google.py +0 -0
  80. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/tts/naoqi.py +0 -0
  81. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/tts/null.py +0 -0
  82. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/vector_store/__init__.py +0 -0
  83. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/vector_store/null.py +0 -0
  84. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/providers/vector_store/redis_store.py +0 -0
  85. {nardial-0.3.3 → nardial-0.4.2}/src/nardial/user_model.py +0 -0
  86. {nardial-0.3.3 → nardial-0.4.2}/src/nardial.egg-info/dependency_links.txt +0 -0
  87. {nardial-0.3.3 → nardial-0.4.2}/src/nardial.egg-info/top_level.txt +0 -0
  88. {nardial-0.3.3 → nardial-0.4.2}/tests/test_event_bus.py +0 -0
  89. {nardial-0.3.3 → nardial-0.4.2}/tests/test_llm.py +0 -0
  90. {nardial-0.3.3 → nardial-0.4.2}/tests/test_mini_dialogs_extra.py +0 -0
  91. {nardial-0.3.3 → nardial-0.4.2}/tests/test_providers.py +0 -0
@@ -1,14 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nardial
3
- Version: 0.3.3
3
+ Version: 0.4.2
4
4
  Summary: Nardial Dialog System
5
5
  Author-email: Mike Ligthart <m.e.u.ligthart@vu.nl>
6
- Maintainer: Clara Fayyad, Landon Brown, Evelyne Prodescu
6
+ Maintainer: Clara Fayyad, Landon Brown, Evelyne Prodescu, Eva Biesot
7
7
  License-Expression: AGPL-3.0-only
8
8
  Requires-Python: >=3.12
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: social-interaction-cloud
12
+ Requires-Dist: pynput>=1.7
12
13
  Provides-Extra: google-tts
13
14
  Requires-Dist: social-interaction-cloud[google-tts]; extra == "google-tts"
14
15
  Provides-Extra: elevenlabs
@@ -42,6 +43,7 @@ It lets you author complete conversations declaratively in JSON, then drive them
42
43
  4. [Defining Dialogs in JSON](#defining-dialogs-in-json)
43
44
  - [Dialog Structure](#dialog-structure)
44
45
  - [Dialog Types](#dialog-types)
46
+ - [Custom dialog types](#custom-dialog-types)
45
47
  - [Move Types](#move-types)
46
48
  - [Key JSON Attributes](#key-json-attributes)
47
49
  5. [Demos / Creating a Session](#demos--creating-a-session)
@@ -157,23 +159,24 @@ NarDialPy is built around a set of provider protocols. Each protocol defines a r
157
159
 
158
160
  ### Available Providers
159
161
 
160
- | Role | Provider | Import path | Requires |
161
- |---|---|---|---|
162
- | **Device** | `DesktopAdapter` | `nardial.providers.device.desktop` | base |
163
- | | `PepperAdapter` | `nardial.providers.device.pepper` | base |
164
- | | `NaoAdapter` | `nardial.providers.device.nao` | base |
165
- | | `AlphaminiAdapter` | `nardial.providers.device.alphamini` | `social-interaction-cloud[alphamini]` |
166
- | **TTS** | `GoogleTTSProvider` | `nardial.providers.tts.google` | `nardial[google-tts]` |
167
- | | `ElevenLabsTTSProvider` | `nardial.providers.tts.elevenlabs` | `nardial[elevenlabs]` |
168
- | | `NaoqiTTSProvider` | `nardial.providers.tts.naoqi` | base (uses device's built-in TTS) |
169
- | | `NullTTSProvider` | `nardial.providers.tts.null` | base (prints to terminal) |
170
- | **NLU** | `DialogflowNLUProvider` | `nardial.providers.nlu.dialogflow` | `nardial[dialogflow]` |
171
- | | `WrittenKeywordNLUProvider` | `nardial.providers.nlu.written_keyword` | base (keyboard input) |
172
- | **LLM** | `OpenAIGPTProvider` | `nardial.providers.llm.openai_gpt` | `nardial[openai]` |
173
- | | `EchoLLMProvider` | `nardial.providers.llm.echo` | base (echoes user input) |
174
- | **Screen** | `ScreenProvider` / `SICScreenAdapter` / `PepperTabletScreenAdapter` | `nardial.providers.screen` | browser display via SIC webserver |
162
+ | Role | Provider | Import path | Requires |
163
+ |---|---|----------------------------------------------|---|
164
+ | **Device** | `DesktopAdapter` | `nardial.providers.device.desktop` | base |
165
+ | | `PepperAdapter` | `nardial.providers.device.pepper` | base |
166
+ | | `NaoAdapter` | `nardial.providers.device.nao` | base |
167
+ | | `AlphaminiAdapter` | `nardial.providers.device.alphamini` | `social-interaction-cloud[alphamini]` |
168
+ | | `ReachyMiniAdapter` | `nardial.providers.reachy_mini` | `social-interaction-cloud[reachy-mini]` |
169
+ | **TTS** | `GoogleTTSProvider` | `nardial.providers.tts.google` | `nardial[google-tts]` |
170
+ | | `ElevenLabsTTSProvider` | `nardial.providers.tts.elevenlabs` | `nardial[elevenlabs]` |
171
+ | | `NaoqiTTSProvider` | `nardial.providers.tts.naoqi` | base (uses device's built-in TTS) |
172
+ | | `NullTTSProvider` | `nardial.providers.tts.null` | base (prints to terminal) |
173
+ | **NLU** | `DialogflowNLUProvider` | `nardial.providers.nlu.dialogflow` | `nardial[dialogflow]` |
174
+ | | `WrittenKeywordNLUProvider` | `nardial.providers.nlu.written_keyword` | base (keyboard input) |
175
+ | **LLM** | `OpenAIGPTProvider` | `nardial.providers.llm.openai_gpt` | `nardial[openai]` |
176
+ | | `EchoLLMProvider` | `nardial.providers.llm.echo` | base (echoes user input) |
177
+ | **Screen** | `ScreenProvider` / `SICScreenAdapter` / `PepperTabletScreenAdapter` | `nardial.providers.screen` | browser display via SIC webserver |
175
178
  | **Vector store** | `RedisVectorStoreProvider` | `nardial.providers.vector_store.redis_store` | base + running Redis |
176
- | | `NullVectorStoreProvider` | `nardial.providers.vector_store.null` | base |
179
+ | | `NullVectorStoreProvider` | `nardial.providers.vector_store.null` | base |
177
180
 
178
181
  ---
179
182
 
@@ -267,6 +270,37 @@ Then pass `device` and `tts` to `ConversationAgent` as above.
267
270
 
268
271
  ---
269
272
 
273
+ ### Pause and skip controls
274
+
275
+ Bind keys to pause the interaction or to skip a listen that takes too long by passing `controls` in an `InteractionConfig`. Keys work globally (the terminal does not need focus), with or without `SessionManager`:
276
+
277
+ ```python
278
+ from nardial.interaction_orchestrator import InteractionConfig
279
+
280
+ agent = ConversationAgent(
281
+ ..., # device and providers as in the setups above
282
+ int_config=InteractionConfig(
283
+ controls={
284
+ "start_paused": True, # wait for the pause key before starting
285
+ "toggle_pause": ["space"], # pause / resume
286
+ "skip_listen": ["s"], # stop a listen that takes too long
287
+ },
288
+ ),
289
+ )
290
+ ```
291
+
292
+ | Setting | Effect |
293
+ |---|---|
294
+ | `toggle_pause` | Pauses the interaction. Speech in progress is stopped and spoken again from the beginning after resume. Listening in progress is stopped, what was heard so far is kept, and the dialog continues after the listen once resumed. Works in every dialog type, including LLM dialogs. |
295
+ | `skip_listen` | Stops the current listen early. What was said so far is still recognised and used as the answer: Dialogflow gets a `StopListeningMessage`, and local Whisper gets an end-of-audio signal and then transcribes what it recorded (this can take a few seconds; Whisper waits up to 30 s for it, which a faster model can lower with `LocalWhisperNLUProvider(..., stop_grace_seconds=5)`). The written keyword provider cannot stop early and gives up after 3 seconds with an empty answer. |
296
+ | `start_paused` | `true` makes the interaction wait for `toggle_pause` before it starts. |
297
+
298
+ A question whose listen was stopped by a pause or skip is not asked again, even when the answer is empty: the dialog continues with whatever was heard, and an empty answer follows the move's `default_outcome`.
299
+
300
+ Key names are single characters (`"p"`) or [pynput key names](https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key) (`"space"`, `"enter"`, `"f5"`). Moves that are not speech or listening (e.g. `timed_wait`, `wait_for_button`) are not paused; the pause takes effect at the next speech or listen.
301
+
302
+ ---
303
+
270
304
  ### Using SessionManager
271
305
 
272
306
  `SessionManager` wraps `ConversationAgent` and adds dialog loading, eligibility checking, and session state:
@@ -282,6 +316,20 @@ manager = SessionManager(
282
316
  manager.run()
283
317
  ```
284
318
 
319
+ If a previous run crashed mid-session, pass `resume=True` to continue that same incomplete session (one whose `ended_at` is still `None`) instead of starting a new one — dialogs already completed in it are skipped:
320
+
321
+ ```python
322
+ manager = SessionManager(
323
+ agent=agent,
324
+ dialog_file="dialogs/my_dialogs.json",
325
+ participant_id="user_42",
326
+ resume=True,
327
+ )
328
+ manager.run()
329
+ ```
330
+
331
+ See [Developer Flow](docs/DEVELOPER_README.md) for more on session/state handling.
332
+
285
333
  ---
286
334
 
287
335
  ## Defining Dialogs in JSON
@@ -295,10 +343,11 @@ Every dialog has the following shared fields:
295
343
  | Field | Type | Required | Description |
296
344
  |---|---|---|---|
297
345
  | `id` | string | ✅ | Unique identifier referenced in `session_agenda` and `dependencies` |
298
- | `type` | string | ✅ | Dialog type: `"functional"`, `"chitchat"`, `"narrative"`, or `"llm_based"` |
346
+ | `type` | string | ✅ | Dialog type: `"functional"`, `"chitchat"`, `"narrative"`, `"llm_based"`, or a [custom type](#custom-dialog-types) |
299
347
  | `moves` | array | ✅ | Ordered list of move objects the robot will perform |
300
348
  | `dependencies` | array of strings | | Dialog IDs that must have been completed before this dialog may run |
301
349
  | `variable_dependencies` | array | | Variables that must exist in the user model before this dialog may run |
350
+ | `prerequisites` | array | | Actions run just before the dialog's first move: call a registered Python function, or set user-model variables (see [below](#optional-top-level-prerequisites)) |
302
351
 
303
352
  Example:
304
353
  ```json
@@ -338,6 +387,51 @@ Dialogs can define reusable character voices:
338
387
  - Each value must contain a `voice_settings` object.
339
388
  - `voice_settings` is validated at runtime against the active TTS provider.
340
389
 
390
+ #### Optional Top-Level `prerequisites`
391
+
392
+ Dialogs can list actions to run just before their first move, each time the dialog runs:
393
+
394
+ ```json
395
+ {
396
+ "id": "multiplication",
397
+ "type": "chitchat",
398
+ "prerequisites": [
399
+ { "execute": "choose_math_level", "args": [true, false] , "skip_dialog": true},
400
+ { "set_variable": { "math_left": 4, "math_right": 3 } }
401
+ ],
402
+ "moves": [
403
+ { "type": "say", "text": "What is %math_left% times %math_right%?" }
404
+ ]
405
+ }
406
+ ```
407
+
408
+ Each entry does one of two things:
409
+
410
+ | Key | Value | What happens |
411
+ |---|---|---|
412
+ | `execute` | name of a registered function | Calls that Python function. Optional `args`: a list is passed as separate arguments (`[3, 4]` calls `f(3, 4)`), any other value as a single argument, and without `args` the function is called with none. |
413
+ | `set_variable` | object | Merges the object into the user model, so its values can be used in `%variable%` placeholders and later dialogs. |
414
+
415
+ In every prerequisite, `"skip_dialog` can be set to `true` of `false`, based on whether the dialog needs to be skipped if the prerequisite cannot be completed. `"skip_dialog:true` means that the dialog will be skipped if the prerequisite is not completed. If `"skip_dialog: false` is chosen, the dialog will still run, even if the prerequisite could not be completed. The default is `false`.
416
+ Functions are registered in Python under the name used in `execute`, before the session runs:
417
+
418
+ ```python
419
+ from nardial.mini_dialogs import MiniDialog
420
+
421
+ @MiniDialog.register("choose_math_level")
422
+ def choose_math_level(last_correct, needed_help):
423
+ if last_correct and not needed_help:
424
+ level = 2
425
+ else:
426
+ level = 1
427
+ return level
428
+
429
+ ```
430
+
431
+ - Prerequisites run in the order listed, after the dialog has been picked. They can't change whether the dialog itself is eligible, only what happens once it runs.
432
+ - Registered functions must be regular (not `async`) functions. They get only the `args` from the JSON, not the dialog or the user model.
433
+ - If a function isn't registered or raises an error, the error is printed and the remaining prerequisites and the dialog still run.
434
+
341
435
  ---
342
436
 
343
437
  #### 1.`functional`
@@ -465,6 +559,95 @@ A fully LLM-driven dialog where the robot and user engage in a free-form multi-t
465
559
  }
466
560
  ```
467
561
 
562
+ #### Custom dialog types
563
+
564
+ You can define your own dialog type in JSON. A custom type is based on an existing type and changes the rules that decide when its dialogs may run.
565
+
566
+ Type definitions go in their own JSON file (or a directory of them), separate from your dialogs. Mixing them is an error: a type definition in a dialog file, or a dialog in a types file, is reported and skipped. Pass the types file to `SessionManager` as `dialog_types_path`. It is loaded before the dialogs, so dialogs can use the new types:
567
+
568
+ ```python
569
+ manager = SessionManager(
570
+ session_agenda=[...],
571
+ agent=agent,
572
+ dialog_types_path="dialog_types/my_types.json",
573
+ dialog_json_path="dialog_json/my_dialogs.json",
574
+ )
575
+ ```
576
+
577
+ Without `SessionManager`, call `load_dialog_types(path)` (from `nardial.authoring`) before loading the dialogs. Within the types file(s), the order of definitions doesn't matter, even when one type extends another.
578
+
579
+ A type definition looks like this:
580
+
581
+ ```json
582
+ {
583
+ "define_type": "repeatable_chitchat",
584
+ "extends": "chitchat",
585
+ "description": "Chitchat that may come back in later sessions, but runs at most once per session.",
586
+ "remove_rules": ["exclude_if_seen"],
587
+ "add_rules": [{ "rule": "exclude_if_seen", "scope": "session" }]
588
+ }
589
+ ```
590
+
591
+ | Field | Type | Required | Description |
592
+ |---|---|---|---|
593
+ | `define_type` | string | ✅ | Name of the new type, used as `type` in dialogs |
594
+ | `extends` | string | ✅ | Type it is based on: a built-in type or another custom type |
595
+ | `description` | string | | Free-text description |
596
+ | `remove_rules` | array of strings | | Names of the base type's rules to drop |
597
+ | `add_rules` | array | | Rules to add: a rule name, or an object with `"rule"` plus that rule's settings |
598
+
599
+ Dialogs of the new type use the same fields as dialogs of the type it extends:
600
+
601
+ ```json
602
+ {
603
+ "id": "music_lately",
604
+ "type": "repeatable_chitchat",
605
+ "topics": ["music"],
606
+ "moves": [{ "type": "say", "text": "What music have you been listening to lately?" }]
607
+ }
608
+ ```
609
+
610
+ They are also picked by the same agenda slots. For example, a `chitchat_slot` can choose a `repeatable_chitchat` dialog.
611
+
612
+ **Built-in types and their rules:**
613
+
614
+ | Type | Rules |
615
+ |---|---|
616
+ | `functional` | `dependency_met` |
617
+ | `narrative` | `exclude_if_seen`, `dependency_met`, `variable_dependency_met`, `narrative_ordering` |
618
+ | `chitchat` | `exclude_if_seen`, `dependency_met`, `variable_dependency_met` |
619
+ | `llm_based` | `exclude_if_seen`, `dependency_met`, `variable_dependency_met` |
620
+
621
+ **Available rules.** A dialog may run only when all of its type's rules allow it.
622
+
623
+ | Rule | Settings | Allows the dialog to run when |
624
+ |---|---|---|
625
+ | `exclude_if_seen` | `scope`: `"participant"` (default) or `"session"` | it hasn't been completed before (by this participant, or in this session) |
626
+ | `dependency_met` | | every dialog in its `dependencies` has been completed |
627
+ | `variable_dependency_met` | | every required variable in its `variable_dependencies` is set in the user model |
628
+ | `narrative_ordering` | | every earlier `position` in its `thread` has been completed |
629
+
630
+ **Your own rules.** Any other rule is written in Python and registered under a name, which `add_rules` can then use. Settings from the JSON are passed to the constructor:
631
+
632
+ ```python
633
+ from nardial.eligibility import EligibilityRule, register_rule
634
+
635
+ @register_rule("from_session")
636
+ class FromSessionRule(EligibilityRule):
637
+ def __init__(self, min):
638
+ self.min = min
639
+
640
+ def is_eligible(self, dialog, context):
641
+ # context.session_index: session number starting at 1, or None when unknown
642
+ return context.session_index is not None and context.session_index >= self.min
643
+ ```
644
+
645
+ ```json
646
+ { "define_type": "returning_chitchat", "extends": "chitchat", "add_rules": [{ "rule": "from_session", "min": 2 }] }
647
+ ```
648
+
649
+ Import the module that registers the rule before loading your dialogs. A rule can read `context.user_model`, `context.completed_ids`, `context.session_completed_ids`, `context.session_index` and `context.registry`. See `docs/DEVELOPER_README.md` for more, including defining types directly in Python.
650
+
468
651
  ---
469
652
 
470
653
  ### Move Types
@@ -489,6 +672,22 @@ Speaks a piece of text. Variable placeholders (`%variable_name%`) are replaced a
489
672
 
490
673
  ---
491
674
 
675
+ #### `say_options`
676
+
677
+ Speaks one randomly chosen response from a predefined list.
678
+
679
+ | Field | Type | Required | Description |
680
+ |---|---|---|---|
681
+ | `type` | string | ✅ | `"say_options"` |
682
+ | `options` | array of strings | ✅ | Candidate utterances to choose from at runtime |
683
+ | `character` | string | | Optional character name from top-level `characters` |
684
+
685
+ ```json
686
+ { "type": "say_options", "options": ["Hi!", "Hello!", "Hey there!"] }
687
+ ```
688
+
689
+ ---
690
+
492
691
  #### `ask_open`
493
692
 
494
693
  Asks a free-text question and listens for any spoken reply. The answer can be stored in a variable and used to drive branching.
@@ -502,6 +701,8 @@ Asks a free-text question and listens for any spoken reply. The answer can be st
502
701
  | `default_outcome` | string | | Outcome label when no answer or no match is found |
503
702
  | `add_interest_from_answer` | boolean | | If `true`, adds the answer to the user's topics of interest |
504
703
  | `llm_followup` | string | | System prompt for an LLM-generated follow-up sentence after the user replies |
704
+ | `llm_extract_entity` | boolean | | If `true`, the LLM extracts the key entity from the answer (e.g. `"dog"` from `"my favorite animal is a dog"`) before it is stored; unrelated answers store nothing. Default `false` |
705
+ | `timeout` | number | | Seconds to listen for an answer. Defaults to `InteractionConfig(listen_timeout=...)` (10 s) |
505
706
 
506
707
  ```json
507
708
  {
@@ -527,6 +728,7 @@ Asks a yes/no question. The detected intent (`"yes"`, `"no"`, `"dontknow"`) driv
527
728
  | `default_outcome` | string | | Outcome label used as fallback |
528
729
  | `add_interest` | string | | Topic added to interest list when the user answers `"yes"` |
529
730
  | `llm_followup` | string | | System prompt for an LLM-generated follow-up sentence |
731
+ | `timeout` | number | | Seconds to listen for an answer. Defaults to `InteractionConfig(listen_timeout=...)` (10 s) |
530
732
 
531
733
  ```json
532
734
  {
@@ -555,6 +757,7 @@ Presents a multiple-choice question. The selected option drives branching and ca
555
757
  | `default_outcome` | string | | Outcome label when nothing matches |
556
758
  | `add_interest_from_variable` | string | | After storing, adds the named variable's value as a topic of interest |
557
759
  | `llm_followup` | string | | System prompt for an LLM-generated follow-up sentence |
760
+ | `timeout` | number | | Seconds to listen for an answer. Defaults to `InteractionConfig(listen_timeout=...)` (10 s) |
558
761
 
559
762
  ```json
560
763
  {
@@ -601,11 +804,12 @@ Starts a multi-turn LLM-driven exchange *within* an otherwise scripted dialog. U
601
804
 
602
805
  Selects and executes a list of sub-moves based on the current outcome or the value of a user model variable.
603
806
 
604
- | Field | Type | Required | Description |
605
- |---|---|---|---|
606
- | `type` | string | ✅ | `"branch"` |
607
- | `on` | string | ✅ | `"outcome"` to branch on the last question's result, or a variable name to branch on its stored value |
608
- | `cases` | object | ✅ | Maps condition values to arrays of sub-moves |
807
+ | Field | Type | Required | Description |
808
+ |-------------|---|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
809
+ | `type` | string | ✅ | `"branch"` |
810
+ | `on` | string | ✅ | `"outcome"` to branch on the last question's result, `"variables"` to branch based on whether to variabels in the user model have the same value, or a variable name to branch on its stored value |
811
+ | `cases` | object | ✅ | Maps condition values to arrays of sub-moves |
812
+ | `variables` | list | | In case of branching on `"variables"`, a list of the variables that need to be compared |
609
813
 
610
814
  ```json
611
815
  {
@@ -635,6 +839,20 @@ Branching on a stored variable (e.g. to react to an answer from an earlier dialo
635
839
  }
636
840
  ```
637
841
 
842
+ Branching on comparison of two stored variables (e.g. to check whether a given answer is correct). Chooses branch "true" if the values of the variables are the same, branch "false" if not.
843
+
844
+ ```json
845
+ {
846
+ "type": "branch",
847
+ "on": "variables",
848
+ "variables": ["given_answer", "correct_answer"],
849
+ "cases": {
850
+ "true": [{ "type": "say", "text": "That's correct!"}],
851
+ "false": [{ "type": "say", "text": "That's incorrect. Let's try again."}]
852
+ }
853
+ }
854
+ ```
855
+
638
856
  ---
639
857
 
640
858
  #### `play`
@@ -701,14 +919,15 @@ Pause execution for a fixed duration before proceeding to the next move. Useful
701
919
 
702
920
  Suspend dialog execution until a matching web input event arrives or until a timeout elapses. The web UI can present buttons or a short input and emit `web_input` events which this move listens for.
703
921
 
704
- | Field | Type | Required | Description |
705
- |---|---|---|---|
706
- | `type` | string | ✅ | `"wait_for_web_input"` |
707
- | `prompt` | string | | Hint text shown in the web UI (not spoken) |
922
+ | Field | Type | Required | Description |
923
+ |---|---|---|---------------------------------------------------------------------------------|
924
+ | `type` | string | ✅ | `"wait_for_web_input"` |
925
+ | `prompt` | string | | Hint text shown in the web UI (not spoken) |
708
926
  | `options` | array of strings | | Accepted `value` strings from the web event (if omitted, any value is accepted) |
709
- | `timeout` | number | | Seconds to wait before falling back (omit for indefinite wait) |
710
- | `outcomes` | object | | Maps option value → outcome label for branching |
711
- | `default_outcome` | string | | Outcome used on timeout or when no event matches |
927
+ | `timeout` | number | | Seconds to wait before falling back (omit for indefinite wait) |
928
+ | `outcomes` | object | | Maps option value → outcome label for branching |
929
+ | `default_outcome` | string | | Outcome used on timeout or when no event matches |
930
+ | `set_variable` | string | | Stores the user's choice in the user model |
712
931
 
713
932
  ```json
714
933
  {
@@ -717,7 +936,8 @@ Suspend dialog execution until a matching web input event arrives or until a tim
717
936
  "options": ["smile","thumbs","surprised"],
718
937
  "timeout": 15,
719
938
  "outcomes": {"smile": "picked_smile"},
720
- "default_outcome": "no_choice"
939
+ "default_outcome": "no_choice",
940
+ "set_variable": "sticker"
721
941
  }
722
942
  ```
723
943
 
@@ -797,6 +1017,49 @@ Clear the display (show a blank/black screen). No parameters — the next displa
797
1017
 
798
1018
  ---
799
1019
 
1020
+ #### `keyboard_input`
1021
+
1022
+ Display keyboard input on screen. Suspend dialog execution until keyboard input is given or until a timeout elapses.
1023
+
1024
+ | Field | Type | Required | Description |
1025
+ |---|---|---|----------------------------------------------------------------|
1026
+ | `type` | string | ✅ | `"keyboard_input"` |
1027
+ | `prompt` | string | | Hint text shown in the web UI (not spoken) |
1028
+ | `timeout` | number | | Seconds to wait before falling back (omit for indefinite wait) |
1029
+ | `outcomes` | object | | Maps option value → outcome label for branching |
1030
+ | `default_outcome` | string | | Outcome used on timeout or when no event matches |
1031
+ | `set_variable` | string | | Stores the user's choice in the user model |
1032
+
1033
+ ```json
1034
+ {
1035
+ "type": "keyboard_input",
1036
+ "prompt": "How old are you?",
1037
+ "timeout": 15,
1038
+ "default_outcome": "no_age_given",
1039
+ "set_variable": "age"
1040
+ }
1041
+ ```
1042
+
1043
+ ---
1044
+
1045
+ #### `go_to_dialog`
1046
+
1047
+ Move to a specified new minidialog.
1048
+
1049
+ | Field | Type | Required | Description |
1050
+ |-------------------|---|----------|----------------------------------------------------------------|
1051
+ | `type` | string | ✅ | `"go_to_dialog"` |
1052
+ | `dialog_id` | string |✅ | ID of the dialog to go to |
1053
+
1054
+ ```json
1055
+ {
1056
+ "type": "go_to_dialog",
1057
+ "dialog_id": "favorite_animal"
1058
+ }
1059
+ ```
1060
+
1061
+ ---
1062
+
800
1063
  ### Key JSON Attributes
801
1064
 
802
1065
  | Attribute | Where used | Description |
@@ -808,11 +1071,13 @@ Clear the display (show a blank/black screen). No parameters — the next displa
808
1071
 
809
1072
  All you need is a minimal Python script that wires up the device, loads the dialog JSON, and runs the session. You can follow the included demos to get started quickly.
810
1073
 
811
- Two ready-to-run demos are included in the `examples/` directory:
1074
+ Ready-to-run demos are included in the `examples/` directory:
812
1075
  * Demo 1 — General Conversation (`demo_general_conversation.py`): A simple four-step conversation using a mix of narrative and functional dialogs
813
1076
  * Demo 2 — Structured Conversation (`demo_structured_conversation.py`): A more complete example that demonstrates all dialog types and move types, including `ask_llm`, `play`, `motion_sequence`, and `animation`
814
1077
  * Demo 3 — Screen Display (`demo_screen_provider.py`): Shows the browser-based screen UI with transcripts, images, iframes, HTML snippets, buttons, and text input
815
1078
  * Demo 4 — Pepper Tablet (`demo_pepper_tablet.py`): Uses the same screen UI on Pepper's tablet through the SIC webserver
1079
+ * Demo 5 — Agenda System (`demo_agenda_system.py`): Uses every agenda-item type (`narrative_slot`, `chitchat_slot`, `functional_slot`, `llm_dialog_ref`, plain dialog ids) so `SessionManager` picks the next dialog dynamically instead of following a fixed list — see the [Agenda System Guide](docs/AGENDA_SYSTEM_GUIDE.md) for a plain-language walkthrough
1080
+ * Demo 6 — Custom Dialog Type (`demo_custom_dialog_type.py`): Writes a custom eligibility rule in Python and defines two new dialog types in JSON (see [Custom dialog types](#custom-dialog-types)): a chitchat that comes back every session, and one that only runs for participants who said they have a pet
816
1081
 
817
1082
  You can find additional demos in the [SIC Applications repository](https://github.com/Social-AI-VU/sic_applications/tree/main/demos/nardial)
818
1083
 
@@ -820,6 +1085,8 @@ You can find additional demos in the [SIC Applications repository](https://githu
820
1085
 
821
1086
  ## Development
822
1087
 
1088
+ For an architecture-level explanation of how JSON dialogs become running conversations, see [Developer Flow](docs/DEVELOPER_README.md). If you're building an application and want to understand dynamic session agendas rather than runtime internals, see the [Agenda System Guide](docs/AGENDA_SYSTEM_GUIDE.md) instead.
1089
+
823
1090
  Run tests from the repository root:
824
1091
 
825
1092
  ```bash