nardial 0.3.1__tar.gz → 0.3.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 (63) hide show
  1. {nardial-0.3.1 → nardial-0.3.2}/PKG-INFO +1 -1
  2. {nardial-0.3.1 → nardial-0.3.2}/pyproject.toml +1 -1
  3. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/screen/pepper_tablet.py +5 -11
  4. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/screen/sic_adapter.py +80 -96
  5. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/session_manager.py +29 -1
  6. {nardial-0.3.1 → nardial-0.3.2}/src/nardial.egg-info/PKG-INFO +1 -1
  7. {nardial-0.3.1 → nardial-0.3.2}/LICENSE +0 -0
  8. {nardial-0.3.1 → nardial-0.3.2}/README.md +0 -0
  9. {nardial-0.3.1 → nardial-0.3.2}/setup.cfg +0 -0
  10. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/__init__.py +0 -0
  11. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/authoring/__init__.py +0 -0
  12. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/authoring/export_html.py +0 -0
  13. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/authoring/factory.py +0 -0
  14. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/authoring/loader.py +0 -0
  15. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/conversation_agent.py +0 -0
  16. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/conversation_state.py +0 -0
  17. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/dialog_logic.py +0 -0
  18. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/events/__init__.py +0 -0
  19. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/events/bus.py +0 -0
  20. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/events/types.py +0 -0
  21. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/interaction_orchestrator.py +0 -0
  22. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/mini_dialogs.py +0 -0
  23. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/moves.py +0 -0
  24. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/__init__.py +0 -0
  25. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/device/__init__.py +0 -0
  26. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/device/alphamini.py +0 -0
  27. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/device/desktop.py +0 -0
  28. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/device/nao.py +0 -0
  29. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/device/pepper.py +0 -0
  30. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/llm/__init__.py +0 -0
  31. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/llm/echo.py +0 -0
  32. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/llm/openai_gpt.py +0 -0
  33. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/nlu/__init__.py +0 -0
  34. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/nlu/dialogflow.py +0 -0
  35. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/nlu/written_keyword.py +0 -0
  36. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/screen/__init__.py +0 -0
  37. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/screen/null.py +0 -0
  38. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/screen/web/static/screen.css +0 -0
  39. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/screen/web/static/screen.js +0 -0
  40. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/screen/web/static/socket.io.min.js +0 -0
  41. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/screen/web/templates/index.html +0 -0
  42. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/tts/__init__.py +0 -0
  43. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/tts/cacher.py +0 -0
  44. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/tts/elevenlabs.py +0 -0
  45. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/tts/google.py +0 -0
  46. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/tts/naoqi.py +0 -0
  47. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/tts/null.py +0 -0
  48. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/vector_store/__init__.py +0 -0
  49. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/vector_store/null.py +0 -0
  50. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/vector_store/redis_store.py +0 -0
  51. {nardial-0.3.1 → nardial-0.3.2}/src/nardial/user_model.py +0 -0
  52. {nardial-0.3.1 → nardial-0.3.2}/src/nardial.egg-info/SOURCES.txt +0 -0
  53. {nardial-0.3.1 → nardial-0.3.2}/src/nardial.egg-info/dependency_links.txt +0 -0
  54. {nardial-0.3.1 → nardial-0.3.2}/src/nardial.egg-info/requires.txt +0 -0
  55. {nardial-0.3.1 → nardial-0.3.2}/src/nardial.egg-info/top_level.txt +0 -0
  56. {nardial-0.3.1 → nardial-0.3.2}/tests/test_authoring_factory.py +0 -0
  57. {nardial-0.3.1 → nardial-0.3.2}/tests/test_conversation_agent.py +0 -0
  58. {nardial-0.3.1 → nardial-0.3.2}/tests/test_conversation_state.py +0 -0
  59. {nardial-0.3.1 → nardial-0.3.2}/tests/test_event_bus.py +0 -0
  60. {nardial-0.3.1 → nardial-0.3.2}/tests/test_llm.py +0 -0
  61. {nardial-0.3.1 → nardial-0.3.2}/tests/test_mini_dialogs_extra.py +0 -0
  62. {nardial-0.3.1 → nardial-0.3.2}/tests/test_moves.py +0 -0
  63. {nardial-0.3.1 → nardial-0.3.2}/tests/test_providers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nardial
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Nardial Dialog System
5
5
  Author-email: Mike Ligthart <m.e.u.ligthart@vu.nl>
6
6
  Maintainer: Clara Fayyad, Landon Brown, Evelyne Prodescu
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "nardial"
7
- version = "0.3.1"
7
+ version = "0.3.2"
8
8
  description = "Nardial Dialog System"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -37,6 +37,7 @@ constructor. The connection request is sent before the page URL is opened.
37
37
  from __future__ import annotations
38
38
 
39
39
  import logging
40
+ from pathlib import Path
40
41
 
41
42
  from nardial.providers.screen.sic_adapter import SICScreenAdapter
42
43
 
@@ -72,6 +73,8 @@ class PepperTabletScreenAdapter(SICScreenAdapter):
72
73
  wifi_security : str
73
74
  Security type — one of ``"open"``, ``"wep"``, ``"wpa"``, ``"wpa2"``
74
75
  (default: ``"wpa2"``).
76
+ assets_root : str or None
77
+ Optional root directory for local assets (images, videos)
75
78
  """
76
79
 
77
80
  def __init__(
@@ -84,9 +87,10 @@ class PepperTabletScreenAdapter(SICScreenAdapter):
84
87
  wifi_ssid: str | None = None,
85
88
  wifi_password: str = "",
86
89
  wifi_security: str = "wpa2",
90
+ assets_root: Path | None = None,
87
91
  ) -> None:
88
92
  # Parent registers the button-click callback on the webserver.
89
- super().__init__(webserver=webserver)
93
+ super().__init__(webserver=webserver, assets_root=assets_root)
90
94
  self._tablet = tablet
91
95
 
92
96
  if wifi_ssid:
@@ -132,19 +136,9 @@ class PepperTabletScreenAdapter(SICScreenAdapter):
132
136
  except Exception:
133
137
  logger.exception("PepperTabletScreenAdapter: failed to open URL on tablet")
134
138
 
135
- def refresh_tablet(self) -> None:
136
- """Refresh the tablet display by re-sending the current URL.
137
-
138
- This can be used to recover if the tablet goes to sleep or loses the
139
- page for some reason. Failures are logged and swallowed.
140
- """
141
- logger.info("PepperTabletScreenAdapter: refreshing tablet display")
142
- self._open_on_tablet(self.tablet_url)
143
-
144
139
  async def show_image(self, src: str) -> None:
145
140
  """Override parent method to also refresh pepper's tablet."""
146
141
  await super().show_image(src)
147
- self.refresh_tablet()
148
142
 
149
143
  # ------------------------------------------------------------------
150
144
  # Lifecycle
@@ -36,6 +36,7 @@ from __future__ import annotations
36
36
  import base64
37
37
  import logging
38
38
  import mimetypes
39
+ import shutil
39
40
  from pathlib import Path
40
41
  from typing import TYPE_CHECKING
41
42
 
@@ -56,15 +57,80 @@ class SICScreenAdapter(ScreenProvider):
56
57
  webserver : Webserver
57
58
  An already-instantiated SIC ``Webserver`` connector. The caller is
58
59
  responsible for its lifecycle (creation and teardown).
60
+ assets_root : str, optional
61
+ Path to a directory of static assets (e.g. images or videos) to serve.
59
62
  """
60
63
 
61
- def __init__(self, webserver) -> None:
64
+ def __init__(self, webserver, assets_root: Path = None) -> None:
62
65
  self._webserver = webserver
63
66
  self._bus: "EventBus | None" = None
64
67
  # Register the SIC callback immediately. If _bus is None when a click
65
68
  # arrives the event is dropped with a WARNING (see _on_button_clicked).
66
69
  self._webserver.register_callback(self._on_button_clicked)
67
70
 
71
+ # Path to a directory of static assets (e.g. images or videos) to serve.
72
+ self._assets_root = assets_root
73
+
74
+ # Resolve the package's bundled web/static directory.
75
+ try:
76
+ import nardial.providers.screen as _screen_pkg
77
+ self._static_dir = Path(_screen_pkg.__file__).parent / "web" / "static"
78
+ except Exception:
79
+ logger.exception("SICScreenAdapter: failed to determine package static dir")
80
+ self._static_dir = Path.cwd() / "static"
81
+
82
+ self._asset_link_dir = self._static_dir / "user_assets"
83
+ self._ensure_asset_symlink()
84
+
85
+ def _ensure_asset_symlink(self) -> None:
86
+ try:
87
+ if not self._assets_root:
88
+ return
89
+
90
+ if not self._assets_root.exists():
91
+ logger.warning("Asset root does not exist: %s", self._assets_root)
92
+ return
93
+
94
+ self._static_dir.mkdir(parents=True, exist_ok=True)
95
+
96
+ if self._asset_link_dir.exists() or self._asset_link_dir.is_symlink():
97
+ try:
98
+ self._asset_link_dir.unlink()
99
+ except Exception:
100
+ logger.exception("Failed to remove old asset link")
101
+
102
+ try:
103
+ self._asset_link_dir.symlink_to(
104
+ self._assets_root,
105
+ target_is_directory=True
106
+ )
107
+ self._asset_mode = "symlink"
108
+ except Exception:
109
+ logger.warning("Symlink failed; assets will not be served via /static")
110
+ self._asset_mode = "fallback"
111
+
112
+ except Exception:
113
+ logger.exception("Asset setup failed")
114
+
115
+ @staticmethod
116
+ def _resolve_media_src(src: str) -> str:
117
+ if not src:
118
+ return src
119
+
120
+ src = str(src)
121
+
122
+ # Already valid for browser
123
+ if src.startswith(("http://", "https://", "data:")):
124
+ return src
125
+
126
+ # Asset handling
127
+ if src.startswith("assets/"):
128
+ relative = src[len("assets/"):]
129
+ return f"/static/user_assets/{relative}"
130
+
131
+ # fallback: assume it's already a valid static path or malformed input
132
+ return src
133
+
68
134
  # ------------------------------------------------------------------
69
135
  # EventBus wiring (called by SessionManager.run_async)
70
136
  # ------------------------------------------------------------------
@@ -160,104 +226,13 @@ class SICScreenAdapter(ScreenProvider):
160
226
 
161
227
  async def show_image(self, src: str) -> None:
162
228
  """Display an image from a local path or URL. """
163
- resolved = self._resolve_image_src(src)
229
+ resolved = self._resolve_media_src(src)
164
230
  self._send_screen({"type": "image", "src": resolved})
165
231
 
166
- def _resolve_image_src(self, image_path: str) -> str:
167
- """Resolve an image path to a browser-usable src.
168
-
169
- - If the path is an absolute URL (http/https/data) it is returned unchanged.
170
- - If the file exists inside the package web static dir it is rewritten to
171
- the webserver's /static/... URL so the webserver can serve it.
172
- - Otherwise, if a matching file exists on disk (project root, examples,
173
- or absolute path), it is embedded as a base64 data URI so the browser
174
- will display it without requiring the webserver to expose the file.
175
- - If nothing is found, the original image_path is returned unchanged.
176
- """
177
- if not image_path:
178
- return image_path
179
- image_path = str(image_path)
180
- lowered = image_path.lower()
181
- if lowered.startswith("http://") or lowered.startswith("https://") or lowered.startswith("data:"):
182
- return image_path
183
-
184
- # Attempt to find the file on disk. Consider common locations:
185
- # - packaged web static dir (served at /static/...)
186
- # - project root / examples / provided relative path
187
- try:
188
- this_file = Path(__file__).resolve()
189
- # project root: ../../../../ (src/nardial/providers/screen -> go up 4)
190
- project_root = this_file.parents[4]
191
- except Exception:
192
- project_root = Path(".")
193
-
194
- # 1) Check packaged web static dir (src/nardial/providers/screen/web/static)
195
- web_static = Path(__file__).resolve().parent / "web" / "static"
196
- candidate = web_static / image_path
197
- if candidate.exists():
198
- # expose via the webserver's /static/ path
199
- # normalize forward slashes for the URL
200
- rel = candidate.relative_to(web_static).as_posix()
201
- return f"/static/{rel}"
202
-
203
- # 2) Check project locations: project_root / image_path and project_root / examples / image_path
204
- candidates = [
205
- project_root / image_path,
206
- project_root / "examples" / image_path,
207
- Path(image_path),
208
- ]
209
- for c in candidates:
210
- if c.exists():
211
- try:
212
- mime, _ = mimetypes.guess_type(str(c))
213
- if not mime:
214
- mime = "application/octet-stream"
215
- data = c.read_bytes()
216
- b64 = base64.b64encode(data).decode("ascii")
217
- return f"data:{mime};base64,{b64}"
218
- except Exception:
219
- logger.exception("Failed to embed image '%s' as data URI", c)
220
- break
221
-
222
- # Nothing found — return original path and let the browser attempt to load it.
223
- return image_path
224
-
225
- def create_image_page(self, image_path: str):
226
- """Create HTML page showing current image."""
227
- resolved_src = self._resolve_image_src(image_path)
228
- return f"""<!DOCTYPE html>
229
- <html>
230
- <head>
231
- <meta charset="UTF-8">
232
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
233
- <title>Image</title>
234
- <style>
235
- body {{
236
- margin: 0;
237
- padding: 0;
238
- background-color: #000;
239
- display: flex;
240
- align-items: center;
241
- justify-content: center;
242
- height: 100vh;
243
- overflow: hidden;
244
- }}
245
- img {{
246
- max-width: 100%;
247
- max-height: 100%;
248
- object-fit: contain;
249
- }}
250
- </style>
251
- </head>
252
- <body>
253
- <img src="{resolved_src}" alt="Displayed Image">
254
- </body>
255
- </html>
256
- """
257
-
258
232
  async def show_video(self, src: str) -> None:
259
233
  """Display a video from a local path or an embeddable URL."""
260
- self._send_screen({"type": "video", "src": src})
234
+ resolved = self._resolve_media_src(src)
235
+ self._send_screen({"type": "video", "src": resolved})
261
236
 
262
237
  async def show_iframe(self, url: str) -> None:
263
238
  """Embed an external URL in an iframe on the screen."""
@@ -285,4 +260,13 @@ class SICScreenAdapter(ScreenProvider):
285
260
  self._send_screen({"type": "black"})
286
261
 
287
262
  async def close(self) -> None:
288
- """No-op — the SIC ``Webserver`` connector is managed by the caller."""
263
+ # Clean up any symlink created for the assets root.
264
+ if self._asset_mode == "symlink":
265
+ try:
266
+ self._asset_link_dir.unlink()
267
+ except Exception:
268
+ logger.exception("Failed to remove asset symlink")
269
+
270
+ # No other shutdown actions here; the webserver is owned by the caller.
271
+
272
+
@@ -97,7 +97,35 @@ class SessionManager:
97
97
  return session_block
98
98
 
99
99
  def run(self):
100
- asyncio.run(self.run_async())
100
+ """Run the session synchronously and ensure a graceful shutdown.
101
+
102
+ This wraps asyncio.run(self.run_async()) and guarantees that the
103
+ agent's orchestrator is disconnected (providers closed) even if
104
+ the session raises an exception.
105
+ """
106
+ try:
107
+ asyncio.run(self.run_async())
108
+ finally:
109
+ # Attempt best-effort graceful shutdown of provider resources.
110
+ try:
111
+ if hasattr(self.agent, "orchestrator") and self.agent.orchestrator is not None:
112
+ try:
113
+ # Stop any logging thread first (if used)
114
+ if hasattr(self.agent.orchestrator, "stop_logging"):
115
+ try:
116
+ self.agent.orchestrator.stop_logging()
117
+ except Exception:
118
+ pass
119
+ # Disconnect providers (TTS, device, vector store, screen)
120
+ if hasattr(self.agent.orchestrator, "disconnect"):
121
+ self.agent.orchestrator.disconnect()
122
+ except Exception:
123
+ # Swallow errors during shutdown but print for debug
124
+ print("[WARN] Error while disconnecting orchestrator:", end=" ")
125
+ import traceback
126
+ traceback.print_exc()
127
+ except Exception:
128
+ pass
101
129
 
102
130
  async def run_async(self):
103
131
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nardial
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Nardial Dialog System
5
5
  Author-email: Mike Ligthart <m.e.u.ligthart@vu.nl>
6
6
  Maintainer: Clara Fayyad, Landon Brown, Evelyne Prodescu
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes