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.
- {nardial-0.3.1 → nardial-0.3.2}/PKG-INFO +1 -1
- {nardial-0.3.1 → nardial-0.3.2}/pyproject.toml +1 -1
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/screen/pepper_tablet.py +5 -11
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/screen/sic_adapter.py +80 -96
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/session_manager.py +29 -1
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial.egg-info/PKG-INFO +1 -1
- {nardial-0.3.1 → nardial-0.3.2}/LICENSE +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/README.md +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/setup.cfg +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/__init__.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/authoring/__init__.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/authoring/export_html.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/authoring/factory.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/authoring/loader.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/conversation_agent.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/conversation_state.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/dialog_logic.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/events/__init__.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/events/bus.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/events/types.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/interaction_orchestrator.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/mini_dialogs.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/moves.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/__init__.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/device/__init__.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/device/alphamini.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/device/desktop.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/device/nao.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/device/pepper.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/llm/__init__.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/llm/echo.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/llm/openai_gpt.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/nlu/__init__.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/nlu/dialogflow.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/nlu/written_keyword.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/screen/__init__.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/screen/null.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/screen/web/static/screen.css +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/screen/web/static/screen.js +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/screen/web/static/socket.io.min.js +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/screen/web/templates/index.html +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/tts/__init__.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/tts/cacher.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/tts/elevenlabs.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/tts/google.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/tts/naoqi.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/tts/null.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/vector_store/__init__.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/vector_store/null.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/providers/vector_store/redis_store.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial/user_model.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial.egg-info/SOURCES.txt +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial.egg-info/dependency_links.txt +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial.egg-info/requires.txt +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/src/nardial.egg-info/top_level.txt +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/tests/test_authoring_factory.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/tests/test_conversation_agent.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/tests/test_conversation_state.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/tests/test_event_bus.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/tests/test_llm.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/tests/test_mini_dialogs_extra.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/tests/test_moves.py +0 -0
- {nardial-0.3.1 → nardial-0.3.2}/tests/test_providers.py +0 -0
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
"""
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|