dash-ai-chatbot 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,70 @@
1
+ """Asyntai AI chat assistant for Plotly Dash apps.
2
+
3
+ Usage::
4
+
5
+ from dash import Dash, html
6
+ import dash_ai_chatbot
7
+
8
+ app = Dash(__name__)
9
+ app.layout = html.Div('Hello')
10
+ dash_ai_chatbot.init_app(app, widget_id='asyntai_ab12cd34')
11
+
12
+ Or, on Dash 3 and later, set ``ASYNTAI_WIDGET_ID`` in the environment and
13
+ change no code at all.
14
+ """
15
+ from ._core import (
16
+ DEFAULT_SCRIPT_URL,
17
+ clean_script_url,
18
+ clean_widget_id,
19
+ insert_widget,
20
+ widget_snippet,
21
+ )
22
+
23
+ __version__ = '1.0.0'
24
+
25
+ __all__ = [
26
+ 'DEFAULT_SCRIPT_URL',
27
+ 'clean_script_url',
28
+ 'clean_widget_id',
29
+ 'init_app',
30
+ 'insert_widget',
31
+ 'widget_snippet',
32
+ ]
33
+
34
+ _FLAG = '_asyntai_widget_settings'
35
+
36
+
37
+ def init_app(app, widget_id=None, script_url=None):
38
+ """Add the chat widget to every page of a Dash app.
39
+
40
+ ``widget_id`` is the ID from the Install page of the Asyntai dashboard,
41
+ or the whole snippet copied from there. When it is left out, the
42
+ ``ASYNTAI_WIDGET_ID`` environment variable is read each time the page is
43
+ built. ``script_url`` is only for a case where Asyntai support asks you
44
+ to change it.
45
+
46
+ Calling it twice on the same app only updates the values. The app is
47
+ returned, so the call can be chained.
48
+ """
49
+ if not hasattr(app, 'interpolate_index'):
50
+ raise TypeError('init_app() needs a dash.Dash app.')
51
+
52
+ settings = {'widget_id': widget_id, 'script_url': script_url}
53
+
54
+ if getattr(app, _FLAG, None) is not None:
55
+ getattr(app, _FLAG).update(settings)
56
+ return app
57
+
58
+ setattr(app, _FLAG, settings)
59
+ original = app.interpolate_index
60
+
61
+ def interpolate_index(**kwargs):
62
+ page = original(**kwargs)
63
+ current = getattr(app, _FLAG)
64
+ return insert_widget(page, current['widget_id'], current['script_url'])
65
+
66
+ # Wrapping the instance method works on Dash 2, 3 and 4, and it also
67
+ # works when the app sets its own index_string or overrides
68
+ # interpolate_index in a subclass.
69
+ app.interpolate_index = interpolate_index
70
+ return app
@@ -0,0 +1,144 @@
1
+ """Everything the package decides about the widget lives here.
2
+
3
+ Dash serves one HTML page for the whole app, built by ``Dash.interpolate_index``
4
+ and then passed through the ``index`` hooks. The widget goes into that page
5
+ once, just before the closing body tag, so it is on every view of the app,
6
+ including multi-page apps built with ``dash.page_registry``.
7
+ """
8
+ import html
9
+ import os
10
+ import re
11
+
12
+ DEFAULT_SCRIPT_URL = 'https://widget.asyntai.com/static/js/chat-widget.js'
13
+
14
+ #: Environment variables read when no value is passed in code.
15
+ ENV_WIDGET_ID = 'ASYNTAI_WIDGET_ID'
16
+ ENV_SCRIPT_URL = 'ASYNTAI_SCRIPT_URL'
17
+
18
+ #: Marker that shows the widget is already on the page.
19
+ MARKER = 'asyntai-widget-config'
20
+
21
+ #: Shape of a widget identifier from the Asyntai dashboard.
22
+ ID_PATTERN = re.compile(r'^asyntai_[A-Za-z0-9]{6,64}$')
23
+
24
+ #: The identifier inside a pasted snippet.
25
+ SNIPPET_PATTERN = re.compile(r'data-asyntai-id\s*=\s*["\']([^"\']+)["\']')
26
+
27
+ _HTTPS_PATTERN = re.compile(r'^https://[^\s"\'<>]+$', re.IGNORECASE)
28
+
29
+ BODY_CLOSE = '</body>'
30
+
31
+ _LOADER = """<div class="asyntai-widget-config" data-asyntai-widget="{widget_id}" data-asyntai-src="{script_url}" style="display: none"></div>
32
+ <script>
33
+ // Loads the Asyntai chat widget after the page has finished loading, so
34
+ // the Dash app draws first. The two values arrive on data attributes,
35
+ // because nothing is ever written into the script itself.
36
+ (function () {{
37
+ var config = document.querySelector('.asyntai-widget-config');
38
+ if (!config) {{ return; }}
39
+
40
+ var widgetId = config.getAttribute('data-asyntai-widget') || '';
41
+ var scriptUrl = config.getAttribute('data-asyntai-src') || '';
42
+ if (!widgetId || scriptUrl.indexOf('https://') !== 0) {{ return; }}
43
+
44
+ var load = function () {{
45
+ if (document.querySelector('script[data-asyntai-id]')) {{ return; }}
46
+
47
+ var script = document.createElement('script');
48
+ script.src = scriptUrl;
49
+ script.async = true;
50
+ script.setAttribute('data-asyntai-id', widgetId);
51
+ document.head.appendChild(script);
52
+ }};
53
+
54
+ if (document.readyState === 'complete') {{
55
+ load();
56
+ }} else {{
57
+ window.addEventListener('load', load);
58
+ }}
59
+ }}());
60
+ </script>
61
+ """
62
+
63
+
64
+ def clean_widget_id(raw):
65
+ """Return a usable widget identifier, or an empty string.
66
+
67
+ Accepts a bare identifier or the whole snippet from the Asyntai
68
+ dashboard, because people paste the snippet. An empty string means the
69
+ chat stays off.
70
+ """
71
+ if not isinstance(raw, str):
72
+ return ''
73
+
74
+ value = raw.strip()
75
+ if not value:
76
+ return ''
77
+
78
+ match = SNIPPET_PATTERN.search(value)
79
+ if match:
80
+ value = match.group(1).strip()
81
+
82
+ return value if ID_PATTERN.match(value) else ''
83
+
84
+
85
+ def clean_script_url(raw):
86
+ """Return the script address, or an empty string when the value is bad.
87
+
88
+ Only an https address is accepted, so a bad value can never put a
89
+ javascript: URL on the page. None or an empty value means the Asyntai
90
+ address.
91
+ """
92
+ if raw is None:
93
+ return DEFAULT_SCRIPT_URL
94
+ if not isinstance(raw, str):
95
+ return ''
96
+
97
+ value = raw.strip()
98
+ if not value:
99
+ return DEFAULT_SCRIPT_URL
100
+
101
+ return value if _HTTPS_PATTERN.match(value) else ''
102
+
103
+
104
+ def widget_snippet(widget_id, script_url=None):
105
+ """Return the HTML that loads the widget, or an empty string.
106
+
107
+ The empty string comes back when the identifier or the address is not
108
+ valid, so a typo leaves the app as it was instead of breaking it.
109
+ """
110
+ clean_id = clean_widget_id(widget_id)
111
+ clean_url = clean_script_url(script_url)
112
+ if not clean_id or not clean_url:
113
+ return ''
114
+
115
+ return _LOADER.format(
116
+ widget_id=html.escape(clean_id, quote=True),
117
+ script_url=html.escape(clean_url, quote=True),
118
+ )
119
+
120
+
121
+ def insert_widget(page, widget_id=None, script_url=None):
122
+ """Return the page with the widget added before the closing body tag.
123
+
124
+ Values passed in come first; missing ones are read from the environment
125
+ at the moment the page is built, so a deployment can change the ID
126
+ without a code change. The page is returned unchanged when there is no
127
+ valid ID, when it has no body tag, or when the widget is already on it.
128
+ """
129
+ if not isinstance(page, str) or MARKER in page:
130
+ return page
131
+
132
+ if widget_id is None:
133
+ widget_id = os.environ.get(ENV_WIDGET_ID, '')
134
+ if script_url is None:
135
+ script_url = os.environ.get(ENV_SCRIPT_URL) or None
136
+
137
+ snippet = widget_snippet(widget_id, script_url)
138
+ if not snippet or BODY_CLOSE not in page:
139
+ return page
140
+
141
+ # Split on the last closing tag, so a page that prints the tag inside an
142
+ # example or a code block is left alone.
143
+ head, _, tail = page.rpartition(BODY_CLOSE)
144
+ return head + snippet + BODY_CLOSE + tail
@@ -0,0 +1,25 @@
1
+ """Zero-code mode for Dash 3 and later.
2
+
3
+ Dash loads every module named in a ``dash_hooks`` entry point when an app
4
+ starts. This one registers an ``index`` hook, so the widget appears on every
5
+ Dash app on the machine once ``ASYNTAI_WIDGET_ID`` is set, with no code
6
+ change. Without that variable the hook leaves every page as it was.
7
+
8
+ Older Dash versions have no hooks; there ``init_app`` does the same job.
9
+ """
10
+ from ._core import insert_widget
11
+
12
+ try:
13
+ from dash import hooks
14
+ except ImportError: # Dash 2 has no hooks module.
15
+ hooks = None
16
+
17
+
18
+ def _add_widget(page):
19
+ # Only the environment is read here. An app that called init_app() has
20
+ # already got the widget, and insert_widget() sees the marker and stops.
21
+ return insert_widget(page)
22
+
23
+
24
+ if hooks is not None and hasattr(hooks, 'index'):
25
+ hooks.index()(_add_widget)
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: dash-ai-chatbot
3
+ Version: 1.0.0
4
+ Summary: Adds the Asyntai AI chat assistant to a Plotly Dash app. It answers visitor questions from your own content, in more than 80 languages.
5
+ Author-email: Asyntai <hello@asyntai.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://asyntai.com
8
+ Project-URL: Documentation, https://asyntai.com/documentation/integrations/plotly-dash/
9
+ Project-URL: Source, https://github.com/asyntai/dash-ai-chatbot
10
+ Project-URL: Issues, https://github.com/asyntai/dash-ai-chatbot/issues
11
+ Keywords: dash,plotly,plotly-dash,chatbot,ai,ai-chatbot,live-chat,customer-support,assistant,widget
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Environment :: Web Environment
14
+ Classifier: Framework :: Dash
15
+ Classifier: Framework :: Flask
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: dash>=2.0
28
+ Dynamic: license-file
29
+
30
+ # dash-ai-chatbot
31
+
32
+ Adds the [Asyntai](https://asyntai.com) AI chat assistant to a
33
+ [Plotly Dash](https://dash.plotly.com) app. The assistant answers visitor
34
+ questions from your own content, day and night, in more than 80 languages.
35
+
36
+ Free and open source. Works on every Asyntai plan, including the free plan.
37
+
38
+ - Requires Dash 2.0 or newer and Python 3.10 or newer
39
+ - Documentation: https://asyntai.com/documentation/integrations/plotly-dash/
40
+
41
+ ## Install
42
+
43
+ ```
44
+ pip install dash-ai-chatbot
45
+ ```
46
+
47
+ Add one line after you create the app:
48
+
49
+ ```python
50
+ from dash import Dash, html
51
+ import dash_ai_chatbot
52
+
53
+ app = Dash(__name__)
54
+ app.layout = html.Div("Hello")
55
+
56
+ dash_ai_chatbot.init_app(app, widget_id="asyntai_ab12cd34")
57
+
58
+ if __name__ == "__main__":
59
+ app.run()
60
+ ```
61
+
62
+ You get the ID from the Install page of your
63
+ [Asyntai dashboard](https://asyntai.com/dashboard). You can paste the whole
64
+ snippet; the package keeps only the ID.
65
+
66
+ That is all. The chat button appears on every page of the app, including
67
+ multi-page apps.
68
+
69
+ ## No code change (Dash 3 and later)
70
+
71
+ On Dash 3 and later the package also works with no code change. Install it and
72
+ set one environment variable:
73
+
74
+ ```
75
+ ASYNTAI_WIDGET_ID=asyntai_ab12cd34
76
+ ```
77
+
78
+ Every Dash app that runs with that variable gets the chat. Without the
79
+ variable the package does nothing.
80
+
81
+ ## Keep the ID out of the code
82
+
83
+ Leave out `widget_id` and the package reads `ASYNTAI_WIDGET_ID` each time the
84
+ page is built:
85
+
86
+ ```python
87
+ dash_ai_chatbot.init_app(app)
88
+ ```
89
+
90
+ ## Options
91
+
92
+ ```python
93
+ dash_ai_chatbot.init_app(
94
+ app,
95
+ widget_id="asyntai_ab12cd34", # or the whole snippet; empty or invalid = chat off
96
+ script_url=None, # change only if Asyntai support asks you to
97
+ )
98
+ ```
99
+
100
+ `script_url` can also come from the `ASYNTAI_SCRIPT_URL` environment variable.
101
+ Only an `https` address is accepted.
102
+
103
+ ## How it works
104
+
105
+ Dash serves one HTML page for the whole app. The package puts a small script
106
+ just before the closing body tag of that page. The script waits for the page to
107
+ finish loading, then fetches the chat widget from Asyntai, so your app draws
108
+ first and its speed is unchanged.
109
+
110
+ The widget ID and the script address travel to the browser on a data
111
+ attribute, never inside the script. The package works with a custom
112
+ `index_string`, with a subclass that overrides `interpolate_index`, and with
113
+ `url_base_pathname`. It never adds the widget twice.
114
+
115
+ ## Tests
116
+
117
+ ```
118
+ pip install -e . pytest
119
+ pytest
120
+ ```
121
+
122
+ ## Support
123
+
124
+ hello@asyntai.com
125
+
126
+ ## Licence
127
+
128
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,9 @@
1
+ dash_ai_chatbot/__init__.py,sha256=EEIEG-_1V0Wv3xFYz9XrY0fgn0fs_v3ELozfVPppkAM,2018
2
+ dash_ai_chatbot/_core.py,sha256=aGnSgJy3oLetM1bpYF8ICZds2oscTbgW2RmXRqPe-6k,4890
3
+ dash_ai_chatbot/_hooks.py,sha256=oTk6Mz0fwcK2n5x7fWX-KiUYwya8Sas1v48IwHm3emg,835
4
+ dash_ai_chatbot-1.0.0.dist-info/licenses/LICENSE,sha256=jGsodKzosaT2Pr7P3ahTkLrMTHQ6uvf111OMRXeGYUM,1064
5
+ dash_ai_chatbot-1.0.0.dist-info/METADATA,sha256=F5Ui67eDLFKVYbDRrSGLSqEJTdWLsFpHn8OALOFqBes,3961
6
+ dash_ai_chatbot-1.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
7
+ dash_ai_chatbot-1.0.0.dist-info/entry_points.txt,sha256=0ZmnJjX2h3TFKlFRNVSuyeYrWNAxZ1Uj_QgDEgGc1mE,54
8
+ dash_ai_chatbot-1.0.0.dist-info/top_level.txt,sha256=ZMDPPvwvSe1ixS3BS-_qYgGVI8mspvau9wKnNvaAHEE,16
9
+ dash_ai_chatbot-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [dash_hooks]
2
+ dash_ai_chatbot = dash_ai_chatbot._hooks
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Asyntai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ dash_ai_chatbot