ChatterBot 1.2.11__tar.gz → 1.2.12__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.
- {chatterbot-1.2.11 → chatterbot-1.2.12}/ChatterBot.egg-info/PKG-INFO +10 -10
- {chatterbot-1.2.11 → chatterbot-1.2.12}/ChatterBot.egg-info/SOURCES.txt +1 -2
- {chatterbot-1.2.11 → chatterbot-1.2.12}/ChatterBot.egg-info/requires.txt +7 -7
- {chatterbot-1.2.11 → chatterbot-1.2.12}/PKG-INFO +10 -10
- {chatterbot-1.2.11 → chatterbot-1.2.12}/README.md +2 -2
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/__init__.py +1 -1
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/constants.py +4 -4
- chatterbot-1.2.12/chatterbot/ext/django_chatterbot/migrations/0021_increase_text_max_length_to_1100.py +55 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/storage/redis.py +31 -137
- {chatterbot-1.2.11 → chatterbot-1.2.12}/pyproject.toml +7 -7
- chatterbot-1.2.11/tests/test_connection_pool.py +0 -268
- chatterbot-1.2.11/tests/test_poc_vulnerability.py +0 -152
- {chatterbot-1.2.11 → chatterbot-1.2.12}/ChatterBot.egg-info/dependency_links.txt +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/ChatterBot.egg-info/top_level.txt +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/LICENSE +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/__main__.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/adapters.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/chatterbot.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/comparisons.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/components.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/conversation.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/corpus.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/exceptions.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/__init__.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/__init__.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/abstract_models.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/admin.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/apps.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0001_initial.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0002_statement_extra_data.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0003_change_occurrence_default.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0004_rename_in_response_to.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0005_statement_created_at.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0006_create_conversation.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0007_response_created_at.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0008_update_conversations.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0009_tags.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0010_statement_text.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0011_blank_extra_data.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0012_statement_created_at.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0013_change_conversations.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0014_remove_statement_extra_data.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0015_statement_persona.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0016_statement_stemmed_text.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0017_tags_unique.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0018_text_max_length.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0019_alter_statement_id_alter_tag_id_and_more.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0020_alter_statement_conversation_and_more.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/__init__.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/model_admin.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/models.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/settings.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/sqlalchemy_app/__init__.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/sqlalchemy_app/models.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/filters.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/languages.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/llm.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/logic/__init__.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/logic/best_match.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/logic/logic_adapter.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/logic/mathematical_evaluation.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/logic/specific_response.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/logic/time_adapter.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/logic/unit_conversion.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/parsing.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/preprocessors.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/response_selection.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/search.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/storage/__init__.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/storage/django_storage.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/storage/mongodb.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/storage/sql_storage.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/storage/storage_adapter.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/tagging.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/trainers.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/utils.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/vectorstores.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/setup.cfg +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/tests/test_adapter_validation.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/tests/test_benchmarks.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/tests/test_chatbot.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/tests/test_cli.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/tests/test_comparisons.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/tests/test_conversations.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/tests/test_corpus.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/tests/test_examples.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/tests/test_filters.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/tests/test_initialization.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/tests/test_languages.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/tests/test_parsing.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/tests/test_preprocessors.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/tests/test_response_selection.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/tests/test_search.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/tests/test_tagging.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/tests/test_turing.py +0 -0
- {chatterbot-1.2.11 → chatterbot-1.2.12}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ChatterBot
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.12
|
|
4
4
|
Summary: ChatterBot is a machine learning, conversational dialog engine
|
|
5
5
|
Author: Gunther Cox
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -39,7 +39,7 @@ Requires-Dist: tqdm
|
|
|
39
39
|
Provides-Extra: test
|
|
40
40
|
Requires-Dist: flake8; extra == "test"
|
|
41
41
|
Requires-Dist: coverage; extra == "test"
|
|
42
|
-
Requires-Dist: sphinx<
|
|
42
|
+
Requires-Dist: sphinx<9.2,>=5.3; extra == "test"
|
|
43
43
|
Requires-Dist: sphinx-sitemap>=2.6.0; extra == "test"
|
|
44
44
|
Requires-Dist: huggingface_hub; extra == "test"
|
|
45
45
|
Requires-Dist: django<6.0,<=4.1; extra == "test"
|
|
@@ -50,13 +50,13 @@ Requires-Dist: chatterbot-corpus<1.3.0,>=1.2.2; extra == "dev"
|
|
|
50
50
|
Requires-Dist: ollama<1.0,>=0.6.0; extra == "dev"
|
|
51
51
|
Requires-Dist: openai; extra == "dev"
|
|
52
52
|
Provides-Extra: redis
|
|
53
|
-
Requires-Dist: redis[hiredis]<7.0; extra == "redis"
|
|
54
|
-
Requires-Dist: langchain-redis
|
|
55
|
-
Requires-Dist: langchain-huggingface
|
|
56
|
-
Requires-Dist: accelerate
|
|
57
|
-
Requires-Dist: sentence-transformers
|
|
53
|
+
Requires-Dist: redis[hiredis]<7.2,>=7.0; extra == "redis"
|
|
54
|
+
Requires-Dist: langchain-redis<0.3.0; extra == "redis"
|
|
55
|
+
Requires-Dist: langchain-huggingface<1.3.0,>=0.1.2; extra == "redis"
|
|
56
|
+
Requires-Dist: accelerate<1.13,>=1.6.0; extra == "redis"
|
|
57
|
+
Requires-Dist: sentence-transformers<5.3.0,>=4.0.2; extra == "redis"
|
|
58
58
|
Provides-Extra: mongodb
|
|
59
|
-
Requires-Dist: pymongo<4.
|
|
59
|
+
Requires-Dist: pymongo<4.17,>=4.11; extra == "mongodb"
|
|
60
60
|
Dynamic: license-file
|
|
61
61
|
|
|
62
62
|

|
|
@@ -162,9 +162,9 @@ https://docs.chatterbot.us/contributing/
|
|
|
162
162
|
|
|
163
163
|
ChatterBot is sponsored by:
|
|
164
164
|
|
|
165
|
-
<p
|
|
165
|
+
<p>
|
|
166
166
|
<a href="https://www.lambdatest.com/?utm_source=chatterbot&utm_medium=sponsor" target="_blank">
|
|
167
|
-
<img src="
|
|
167
|
+
<img src="docs/_static/testmu-ai-white-logo.png" style="vertical-align: middle;" width="250" height="80" />
|
|
168
168
|
</a>
|
|
169
169
|
</p>
|
|
170
170
|
|
|
@@ -56,6 +56,7 @@ chatterbot/ext/django_chatterbot/migrations/0017_tags_unique.py
|
|
|
56
56
|
chatterbot/ext/django_chatterbot/migrations/0018_text_max_length.py
|
|
57
57
|
chatterbot/ext/django_chatterbot/migrations/0019_alter_statement_id_alter_tag_id_and_more.py
|
|
58
58
|
chatterbot/ext/django_chatterbot/migrations/0020_alter_statement_conversation_and_more.py
|
|
59
|
+
chatterbot/ext/django_chatterbot/migrations/0021_increase_text_max_length_to_1100.py
|
|
59
60
|
chatterbot/ext/django_chatterbot/migrations/__init__.py
|
|
60
61
|
chatterbot/ext/sqlalchemy_app/__init__.py
|
|
61
62
|
chatterbot/ext/sqlalchemy_app/models.py
|
|
@@ -77,7 +78,6 @@ tests/test_benchmarks.py
|
|
|
77
78
|
tests/test_chatbot.py
|
|
78
79
|
tests/test_cli.py
|
|
79
80
|
tests/test_comparisons.py
|
|
80
|
-
tests/test_connection_pool.py
|
|
81
81
|
tests/test_conversations.py
|
|
82
82
|
tests/test_corpus.py
|
|
83
83
|
tests/test_examples.py
|
|
@@ -85,7 +85,6 @@ tests/test_filters.py
|
|
|
85
85
|
tests/test_initialization.py
|
|
86
86
|
tests/test_languages.py
|
|
87
87
|
tests/test_parsing.py
|
|
88
|
-
tests/test_poc_vulnerability.py
|
|
89
88
|
tests/test_preprocessors.py
|
|
90
89
|
tests/test_response_selection.py
|
|
91
90
|
tests/test_search.py
|
|
@@ -12,19 +12,19 @@ ollama<1.0,>=0.6.0
|
|
|
12
12
|
openai
|
|
13
13
|
|
|
14
14
|
[mongodb]
|
|
15
|
-
pymongo<4.
|
|
15
|
+
pymongo<4.17,>=4.11
|
|
16
16
|
|
|
17
17
|
[redis]
|
|
18
|
-
redis[hiredis]<7.0
|
|
19
|
-
langchain-redis
|
|
20
|
-
langchain-huggingface
|
|
21
|
-
accelerate
|
|
22
|
-
sentence-transformers
|
|
18
|
+
redis[hiredis]<7.2,>=7.0
|
|
19
|
+
langchain-redis<0.3.0
|
|
20
|
+
langchain-huggingface<1.3.0,>=0.1.2
|
|
21
|
+
accelerate<1.13,>=1.6.0
|
|
22
|
+
sentence-transformers<5.3.0,>=4.0.2
|
|
23
23
|
|
|
24
24
|
[test]
|
|
25
25
|
flake8
|
|
26
26
|
coverage
|
|
27
|
-
sphinx<
|
|
27
|
+
sphinx<9.2,>=5.3
|
|
28
28
|
sphinx-sitemap>=2.6.0
|
|
29
29
|
huggingface_hub
|
|
30
30
|
django<6.0,<=4.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ChatterBot
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.12
|
|
4
4
|
Summary: ChatterBot is a machine learning, conversational dialog engine
|
|
5
5
|
Author: Gunther Cox
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -39,7 +39,7 @@ Requires-Dist: tqdm
|
|
|
39
39
|
Provides-Extra: test
|
|
40
40
|
Requires-Dist: flake8; extra == "test"
|
|
41
41
|
Requires-Dist: coverage; extra == "test"
|
|
42
|
-
Requires-Dist: sphinx<
|
|
42
|
+
Requires-Dist: sphinx<9.2,>=5.3; extra == "test"
|
|
43
43
|
Requires-Dist: sphinx-sitemap>=2.6.0; extra == "test"
|
|
44
44
|
Requires-Dist: huggingface_hub; extra == "test"
|
|
45
45
|
Requires-Dist: django<6.0,<=4.1; extra == "test"
|
|
@@ -50,13 +50,13 @@ Requires-Dist: chatterbot-corpus<1.3.0,>=1.2.2; extra == "dev"
|
|
|
50
50
|
Requires-Dist: ollama<1.0,>=0.6.0; extra == "dev"
|
|
51
51
|
Requires-Dist: openai; extra == "dev"
|
|
52
52
|
Provides-Extra: redis
|
|
53
|
-
Requires-Dist: redis[hiredis]<7.0; extra == "redis"
|
|
54
|
-
Requires-Dist: langchain-redis
|
|
55
|
-
Requires-Dist: langchain-huggingface
|
|
56
|
-
Requires-Dist: accelerate
|
|
57
|
-
Requires-Dist: sentence-transformers
|
|
53
|
+
Requires-Dist: redis[hiredis]<7.2,>=7.0; extra == "redis"
|
|
54
|
+
Requires-Dist: langchain-redis<0.3.0; extra == "redis"
|
|
55
|
+
Requires-Dist: langchain-huggingface<1.3.0,>=0.1.2; extra == "redis"
|
|
56
|
+
Requires-Dist: accelerate<1.13,>=1.6.0; extra == "redis"
|
|
57
|
+
Requires-Dist: sentence-transformers<5.3.0,>=4.0.2; extra == "redis"
|
|
58
58
|
Provides-Extra: mongodb
|
|
59
|
-
Requires-Dist: pymongo<4.
|
|
59
|
+
Requires-Dist: pymongo<4.17,>=4.11; extra == "mongodb"
|
|
60
60
|
Dynamic: license-file
|
|
61
61
|
|
|
62
62
|

|
|
@@ -162,9 +162,9 @@ https://docs.chatterbot.us/contributing/
|
|
|
162
162
|
|
|
163
163
|
ChatterBot is sponsored by:
|
|
164
164
|
|
|
165
|
-
<p
|
|
165
|
+
<p>
|
|
166
166
|
<a href="https://www.lambdatest.com/?utm_source=chatterbot&utm_medium=sponsor" target="_blank">
|
|
167
|
-
<img src="
|
|
167
|
+
<img src="docs/_static/testmu-ai-white-logo.png" style="vertical-align: middle;" width="250" height="80" />
|
|
168
168
|
</a>
|
|
169
169
|
</p>
|
|
170
170
|
|
|
@@ -101,9 +101,9 @@ https://docs.chatterbot.us/contributing/
|
|
|
101
101
|
|
|
102
102
|
ChatterBot is sponsored by:
|
|
103
103
|
|
|
104
|
-
<p
|
|
104
|
+
<p>
|
|
105
105
|
<a href="https://www.lambdatest.com/?utm_source=chatterbot&utm_medium=sponsor" target="_blank">
|
|
106
|
-
<img src="
|
|
106
|
+
<img src="docs/_static/testmu-ai-white-logo.png" style="vertical-align: middle;" width="250" height="80" />
|
|
107
107
|
</a>
|
|
108
108
|
</p>
|
|
109
109
|
|
|
@@ -5,11 +5,11 @@ from chatterbot import languages
|
|
|
5
5
|
|
|
6
6
|
'''
|
|
7
7
|
The maximum length of characters that the text of a statement can contain.
|
|
8
|
-
The number
|
|
9
|
-
|
|
10
|
-
the data model for each storage adapter.
|
|
8
|
+
The number 1100 is used to support longer conversational statements while
|
|
9
|
+
remaining within VARCHAR limits for most databases. This value should be
|
|
10
|
+
enforced on a per-model basis by the data model for each storage adapter.
|
|
11
11
|
'''
|
|
12
|
-
STATEMENT_TEXT_MAX_LENGTH =
|
|
12
|
+
STATEMENT_TEXT_MAX_LENGTH = 1100
|
|
13
13
|
|
|
14
14
|
'''
|
|
15
15
|
The maximum length of characters that the text label of a conversation can contain.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django migration to increase text field max_length from 255 to 1100.
|
|
3
|
+
|
|
4
|
+
This migration alters all text-related fields in the Statement model:
|
|
5
|
+
- text
|
|
6
|
+
- search_text
|
|
7
|
+
- in_response_to
|
|
8
|
+
- search_in_response_to
|
|
9
|
+
|
|
10
|
+
This change supports longer conversational statements while remaining
|
|
11
|
+
within VARCHAR limits for most databases.
|
|
12
|
+
"""
|
|
13
|
+
from django.db import migrations, models
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Migration(migrations.Migration):
|
|
17
|
+
|
|
18
|
+
dependencies = [
|
|
19
|
+
('django_chatterbot', '0020_alter_statement_conversation_and_more'),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
operations = [
|
|
23
|
+
migrations.AlterField(
|
|
24
|
+
model_name='statement',
|
|
25
|
+
name='text',
|
|
26
|
+
field=models.CharField(max_length=1100, help_text='The text of the statement.'),
|
|
27
|
+
),
|
|
28
|
+
migrations.AlterField(
|
|
29
|
+
model_name='statement',
|
|
30
|
+
name='search_text',
|
|
31
|
+
field=models.CharField(
|
|
32
|
+
blank=True,
|
|
33
|
+
max_length=1100,
|
|
34
|
+
help_text='A modified version of the statement text optimized for searching.'
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
migrations.AlterField(
|
|
38
|
+
model_name='statement',
|
|
39
|
+
name='in_response_to',
|
|
40
|
+
field=models.CharField(
|
|
41
|
+
max_length=1100,
|
|
42
|
+
null=True,
|
|
43
|
+
help_text='The text of the statement that this statement is in response to.'
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
migrations.AlterField(
|
|
47
|
+
model_name='statement',
|
|
48
|
+
name='search_in_response_to',
|
|
49
|
+
field=models.CharField(
|
|
50
|
+
blank=True,
|
|
51
|
+
max_length=1100,
|
|
52
|
+
help_text='A modified version of the in_response_to text optimized for searching.'
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
]
|
|
@@ -65,6 +65,7 @@ class RedisVectorStorageAdapter(StorageAdapter):
|
|
|
65
65
|
index_name='chatterbot',
|
|
66
66
|
redis_url=self.database_uri,
|
|
67
67
|
content_field='in_response_to',
|
|
68
|
+
legacy_key_format=False,
|
|
68
69
|
metadata_schema=[
|
|
69
70
|
{
|
|
70
71
|
'name': 'conversation',
|
|
@@ -212,12 +213,16 @@ class RedisVectorStorageAdapter(StorageAdapter):
|
|
|
212
213
|
- search_in_response_to_contains
|
|
213
214
|
- order_by
|
|
214
215
|
"""
|
|
215
|
-
from redisvl.query import VectorQuery
|
|
216
216
|
from redisvl.query.filter import Tag, Text
|
|
217
217
|
|
|
218
218
|
# https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/query_syntax/
|
|
219
219
|
filter_condition = None
|
|
220
220
|
|
|
221
|
+
ordering = kwargs.get('order_by', None)
|
|
222
|
+
|
|
223
|
+
if ordering:
|
|
224
|
+
ordering = ','.join(ordering)
|
|
225
|
+
|
|
221
226
|
if 'in_response_to' in kwargs:
|
|
222
227
|
filter_condition = Text('in_response_to') == kwargs['in_response_to']
|
|
223
228
|
|
|
@@ -255,7 +260,7 @@ class RedisVectorStorageAdapter(StorageAdapter):
|
|
|
255
260
|
_query = '|'.join([
|
|
256
261
|
f'%%{text}%%' for text in kwargs['exclude_text_words']
|
|
257
262
|
])
|
|
258
|
-
query = Text('text') % f'-({
|
|
263
|
+
query = Text('text') % f'-({_query})'
|
|
259
264
|
if filter_condition:
|
|
260
265
|
filter_condition &= query
|
|
261
266
|
else:
|
|
@@ -290,67 +295,20 @@ class RedisVectorStorageAdapter(StorageAdapter):
|
|
|
290
295
|
# similar responses.
|
|
291
296
|
_search_query = kwargs['search_text_contains']
|
|
292
297
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
query = VectorQuery(
|
|
301
|
-
vector=embedding,
|
|
302
|
-
vector_field_name='embedding',
|
|
303
|
-
return_fields=return_fields,
|
|
304
|
-
num_results=page_size,
|
|
305
|
-
filter_expression=filter_condition
|
|
298
|
+
documents = self.vector_store.similarity_search(
|
|
299
|
+
_search_query,
|
|
300
|
+
k=page_size, # The number of results to return
|
|
301
|
+
return_all=True, # Include the full document with IDs
|
|
302
|
+
filter=filter_condition,
|
|
303
|
+
sort_by=ordering
|
|
306
304
|
)
|
|
307
305
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
Document = self.get_statement_model()
|
|
311
|
-
documents = []
|
|
312
|
-
|
|
313
|
-
# Calculate confidence from vector distances
|
|
306
|
+
# Add confidence scores based on similarity ordering
|
|
314
307
|
# Results are ordered by similarity (best match first)
|
|
315
|
-
for idx,
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
# Convert to confidence: confidence = 1 - distance
|
|
320
|
-
# If vector_score not available, use result order
|
|
321
|
-
vector_score = result.get('vector_score')
|
|
322
|
-
if vector_score is not None:
|
|
323
|
-
# Cosine distance ranges from 0 (identical) to 2 (opposite)
|
|
324
|
-
# Normalize to confidence: 1.0 (identical) to 0.0 (opposite)
|
|
325
|
-
confidence = max(0.0, 1.0 - (float(vector_score) / 2.0))
|
|
326
|
-
else:
|
|
327
|
-
# Fallback: use result order (first result = highest confidence)
|
|
328
|
-
# Start at 0.95 for first result, decay by 0.05 per position
|
|
329
|
-
confidence = max(0.0, 0.95 - (idx * 0.05))
|
|
330
|
-
|
|
331
|
-
# Parse timestamp
|
|
332
|
-
created_at_value = result.get('created_at', 0)
|
|
333
|
-
if isinstance(created_at_value, str):
|
|
334
|
-
created_at = datetime.fromtimestamp(float(created_at_value))
|
|
335
|
-
elif created_at_value:
|
|
336
|
-
created_at = datetime.fromtimestamp(float(created_at_value))
|
|
337
|
-
else:
|
|
338
|
-
created_at = datetime.now()
|
|
339
|
-
|
|
340
|
-
metadata = {
|
|
341
|
-
'text': result.get('text', ''),
|
|
342
|
-
'conversation': result.get('conversation', ''),
|
|
343
|
-
'persona': result.get('persona', ''),
|
|
344
|
-
'tags': result.get('tags', ''),
|
|
345
|
-
'created_at': created_at,
|
|
346
|
-
'confidence': confidence,
|
|
347
|
-
}
|
|
348
|
-
doc = Document(
|
|
349
|
-
page_content=in_response_to,
|
|
350
|
-
metadata=metadata,
|
|
351
|
-
id=result['id']
|
|
352
|
-
)
|
|
353
|
-
documents.append(doc)
|
|
308
|
+
for idx, doc in enumerate(documents):
|
|
309
|
+
# Start at 0.95 for first result, decay by 0.05 per position
|
|
310
|
+
confidence = max(0.0, 0.95 - (idx * 0.05))
|
|
311
|
+
doc.metadata['confidence'] = confidence
|
|
354
312
|
|
|
355
313
|
return [self.model_to_object(document) for document in documents]
|
|
356
314
|
|
|
@@ -379,74 +337,20 @@ class RedisVectorStorageAdapter(StorageAdapter):
|
|
|
379
337
|
if 'search_in_response_to_contains' in kwargs:
|
|
380
338
|
_search_text = kwargs.get('search_in_response_to_contains', '')
|
|
381
339
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
]
|
|
389
|
-
|
|
390
|
-
# Use direct index query via RedisVL
|
|
391
|
-
# langchain's similarity_search has issues with filters in v0.2.4
|
|
392
|
-
# and may not work properly with existing indexes
|
|
393
|
-
# TODO: Look into similarity_search_with_score implementation
|
|
394
|
-
query = VectorQuery(
|
|
395
|
-
vector=embedding,
|
|
396
|
-
vector_field_name='embedding',
|
|
397
|
-
return_fields=return_fields,
|
|
398
|
-
num_results=page_size,
|
|
399
|
-
filter_expression=filter_condition
|
|
340
|
+
documents = self.vector_store.similarity_search(
|
|
341
|
+
_search_text,
|
|
342
|
+
k=page_size, # The number of results to return
|
|
343
|
+
return_all=True, # Include the full document with IDs
|
|
344
|
+
filter=filter_condition,
|
|
345
|
+
sort_by=ordering
|
|
400
346
|
)
|
|
401
347
|
|
|
402
|
-
#
|
|
403
|
-
results = self.vector_store.index.query(query)
|
|
404
|
-
|
|
405
|
-
# Convert results to Document objects
|
|
406
|
-
Document = self.get_statement_model()
|
|
407
|
-
documents = []
|
|
408
|
-
|
|
409
|
-
# Calculate confidence from vector distances
|
|
348
|
+
# Add confidence scores based on similarity ordering
|
|
410
349
|
# Results are ordered by similarity (best match first)
|
|
411
|
-
for idx,
|
|
412
|
-
#
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
# Redis vector_score is cosine distance (lower is better)
|
|
416
|
-
# Convert to confidence: confidence = 1 - distance
|
|
417
|
-
# If vector_score not available, use result order
|
|
418
|
-
vector_score = result.get('vector_score')
|
|
419
|
-
if vector_score is not None:
|
|
420
|
-
# Cosine distance ranges from 0 (identical) to 2 (opposite)
|
|
421
|
-
# Normalize to confidence: 1.0 (identical) to 0.0 (opposite)
|
|
422
|
-
confidence = max(0.0, 1.0 - (float(vector_score) / 2.0))
|
|
423
|
-
else:
|
|
424
|
-
# Fallback: use result order (first result = highest confidence)
|
|
425
|
-
# Start at 0.95 for first result, decay by 0.05 per position
|
|
426
|
-
confidence = max(0.0, 0.95 - (idx * 0.05))
|
|
427
|
-
|
|
428
|
-
# Convert Unix timestamp back to datetime
|
|
429
|
-
# Redis returns numeric fields as strings
|
|
430
|
-
created_at_timestamp = result.get('created_at', '0')
|
|
431
|
-
if created_at_timestamp and created_at_timestamp != '0':
|
|
432
|
-
created_at = datetime.fromtimestamp(float(created_at_timestamp))
|
|
433
|
-
else:
|
|
434
|
-
created_at = datetime.now()
|
|
435
|
-
|
|
436
|
-
metadata = {
|
|
437
|
-
'text': result.get('text', ''),
|
|
438
|
-
'conversation': result.get('conversation', ''),
|
|
439
|
-
'persona': result.get('persona', ''),
|
|
440
|
-
'tags': result.get('tags', ''),
|
|
441
|
-
'created_at': created_at,
|
|
442
|
-
'confidence': confidence,
|
|
443
|
-
}
|
|
444
|
-
doc = Document(
|
|
445
|
-
page_content=in_response_to,
|
|
446
|
-
metadata=metadata,
|
|
447
|
-
id=result['id']
|
|
448
|
-
)
|
|
449
|
-
documents.append(doc)
|
|
350
|
+
for idx, doc in enumerate(documents):
|
|
351
|
+
# Start at 0.95 for first result, decay by 0.05 per position
|
|
352
|
+
confidence = max(0.0, 0.95 - (idx * 0.05))
|
|
353
|
+
doc.metadata['confidence'] = confidence
|
|
450
354
|
else:
|
|
451
355
|
documents = self.vector_store.query_search(
|
|
452
356
|
k=page_size,
|
|
@@ -551,11 +455,8 @@ class RedisVectorStorageAdapter(StorageAdapter):
|
|
|
551
455
|
client = self.vector_store.index.client
|
|
552
456
|
client.delete(statement.id)
|
|
553
457
|
|
|
554
|
-
#
|
|
555
|
-
|
|
556
|
-
if '::' in statement.id:
|
|
557
|
-
key = statement.id.split('::', 1)[1]
|
|
558
|
-
elif ':' in statement.id:
|
|
458
|
+
# Extract the key from the full ID (format: prefix:key)
|
|
459
|
+
if ':' in statement.id:
|
|
559
460
|
key = statement.id.split(':', 1)[1]
|
|
560
461
|
else:
|
|
561
462
|
# If no delimiter found, use the entire ID as the key
|
|
@@ -564,13 +465,6 @@ class RedisVectorStorageAdapter(StorageAdapter):
|
|
|
564
465
|
ids = self.vector_store.add_texts(
|
|
565
466
|
[document.page_content], [metadata], keys=[key]
|
|
566
467
|
)
|
|
567
|
-
|
|
568
|
-
# Normalize the ID to use :: delimiter (if langchain-redis returned single colon)
|
|
569
|
-
if ids and ':' in ids[0] and '::' not in ids[0]:
|
|
570
|
-
# Replace first occurrence of single colon with double colon
|
|
571
|
-
normalized_id = ids[0].replace(':', '::', 1)
|
|
572
|
-
# Update the key in Redis to use the correct format
|
|
573
|
-
client.rename(ids[0], normalized_id)
|
|
574
468
|
else:
|
|
575
469
|
self.vector_store.add_documents([document])
|
|
576
470
|
|
|
@@ -71,7 +71,7 @@ dependencies = [
|
|
|
71
71
|
test = [
|
|
72
72
|
"flake8",
|
|
73
73
|
"coverage",
|
|
74
|
-
"sphinx>=5.3,<
|
|
74
|
+
"sphinx>=5.3,<9.2",
|
|
75
75
|
"sphinx-sitemap>=2.6.0",
|
|
76
76
|
"huggingface_hub",
|
|
77
77
|
"django<=4.1,<6.0"
|
|
@@ -84,12 +84,12 @@ dev = [
|
|
|
84
84
|
"openai"
|
|
85
85
|
]
|
|
86
86
|
redis = [
|
|
87
|
-
"redis[hiredis]
|
|
88
|
-
"langchain-redis
|
|
89
|
-
"langchain-huggingface
|
|
90
|
-
"accelerate
|
|
91
|
-
"sentence-transformers
|
|
87
|
+
"redis[hiredis]>=7.0,<7.2",
|
|
88
|
+
"langchain-redis<0.3.0",
|
|
89
|
+
"langchain-huggingface>=0.1.2,<1.3.0",
|
|
90
|
+
"accelerate>=1.6.0,<1.13",
|
|
91
|
+
"sentence-transformers>=4.0.2,<5.3.0",
|
|
92
92
|
]
|
|
93
93
|
mongodb = [
|
|
94
|
-
"pymongo>=4.11,<4.
|
|
94
|
+
"pymongo>=4.11,<4.17",
|
|
95
95
|
]
|
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Tests for database connection pool management and concurrency.
|
|
3
|
-
|
|
4
|
-
These tests verify that the fixes for the connection pool exhaustion
|
|
5
|
-
vulnerability (CVE-TBD) are working correctly.
|
|
6
|
-
"""
|
|
7
|
-
import threading
|
|
8
|
-
import time
|
|
9
|
-
import unittest
|
|
10
|
-
from chatterbot import ChatBot
|
|
11
|
-
from chatterbot.trainers import ListTrainer
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class ConnectionPoolTestCase(unittest.TestCase):
|
|
15
|
-
"""
|
|
16
|
-
Test cases for database connection pool management.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
def setUp(self):
|
|
20
|
-
"""
|
|
21
|
-
Set up test fixtures before each test.
|
|
22
|
-
"""
|
|
23
|
-
# Use in-memory SQLite for fast testing
|
|
24
|
-
# Note: SQLite doesn't use QueuePool, so pool params are ignored
|
|
25
|
-
self.chatbot = ChatBot(
|
|
26
|
-
'TestBot',
|
|
27
|
-
database_uri='sqlite://',
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
# Train with some basic responses
|
|
31
|
-
trainer = ListTrainer(self.chatbot)
|
|
32
|
-
trainer.train([
|
|
33
|
-
'Hi',
|
|
34
|
-
'Hello!',
|
|
35
|
-
'How are you?',
|
|
36
|
-
'I am doing well.',
|
|
37
|
-
'What is your name?',
|
|
38
|
-
'My name is TestBot.',
|
|
39
|
-
])
|
|
40
|
-
|
|
41
|
-
def tearDown(self):
|
|
42
|
-
"""
|
|
43
|
-
Clean up after each test.
|
|
44
|
-
"""
|
|
45
|
-
self.chatbot.storage.drop()
|
|
46
|
-
self.chatbot.storage.close()
|
|
47
|
-
|
|
48
|
-
def test_concurrent_requests_no_exhaustion(self):
|
|
49
|
-
"""
|
|
50
|
-
Test that concurrent requests don't exhaust the connection pool.
|
|
51
|
-
|
|
52
|
-
This was the original vulnerability - concurrent get_response() calls
|
|
53
|
-
would leak sessions and exhaust the pool.
|
|
54
|
-
"""
|
|
55
|
-
num_threads = 30 # More than pool_size + max_overflow
|
|
56
|
-
responses = []
|
|
57
|
-
errors = []
|
|
58
|
-
|
|
59
|
-
def make_request():
|
|
60
|
-
try:
|
|
61
|
-
response = self.chatbot.get_response('Hi')
|
|
62
|
-
responses.append(str(response))
|
|
63
|
-
except Exception as e:
|
|
64
|
-
errors.append(e)
|
|
65
|
-
|
|
66
|
-
threads = []
|
|
67
|
-
for _ in range(num_threads):
|
|
68
|
-
t = threading.Thread(target=make_request)
|
|
69
|
-
threads.append(t)
|
|
70
|
-
t.start()
|
|
71
|
-
|
|
72
|
-
# Wait for all threads to complete
|
|
73
|
-
for t in threads:
|
|
74
|
-
t.join(timeout=10)
|
|
75
|
-
|
|
76
|
-
# Verify no errors occurred
|
|
77
|
-
self.assertEqual(len(errors), 0,
|
|
78
|
-
f"Connection pool exhaustion occurred: {errors}")
|
|
79
|
-
|
|
80
|
-
# Verify all threads got responses
|
|
81
|
-
self.assertEqual(len(responses), num_threads,
|
|
82
|
-
"Not all threads received responses")
|
|
83
|
-
|
|
84
|
-
def test_rapid_sequential_requests(self):
|
|
85
|
-
"""
|
|
86
|
-
Test that rapid sequential requests properly release connections.
|
|
87
|
-
"""
|
|
88
|
-
num_requests = 50 # More than pool size
|
|
89
|
-
|
|
90
|
-
for i in range(num_requests):
|
|
91
|
-
response = self.chatbot.get_response(f'Request {i}')
|
|
92
|
-
self.assertIsNotNone(response)
|
|
93
|
-
|
|
94
|
-
def test_partial_filter_consumption(self):
|
|
95
|
-
"""
|
|
96
|
-
Test that partially consuming filter() results doesn't leak sessions.
|
|
97
|
-
|
|
98
|
-
This was a key part of the vulnerability - the filter() generator
|
|
99
|
-
would not close the session if iteration stopped early.
|
|
100
|
-
"""
|
|
101
|
-
# Create many statements
|
|
102
|
-
trainer = ListTrainer(self.chatbot)
|
|
103
|
-
for i in range(100):
|
|
104
|
-
trainer.train([f'Question {i}', f'Answer {i}'])
|
|
105
|
-
|
|
106
|
-
# Partially consume filter results many times
|
|
107
|
-
for _ in range(50):
|
|
108
|
-
results = self.chatbot.storage.filter()
|
|
109
|
-
# Only consume first result
|
|
110
|
-
first = next(results, None)
|
|
111
|
-
self.assertIsNotNone(first)
|
|
112
|
-
# Don't consume the rest - this should still clean up the session
|
|
113
|
-
|
|
114
|
-
# If sessions weren't cleaned up, this would fail
|
|
115
|
-
response = self.chatbot.get_response('Hi')
|
|
116
|
-
self.assertIsNotNone(response)
|
|
117
|
-
|
|
118
|
-
def test_concurrent_training(self):
|
|
119
|
-
"""
|
|
120
|
-
Test that concurrent training operations don't leak connections.
|
|
121
|
-
"""
|
|
122
|
-
errors = []
|
|
123
|
-
|
|
124
|
-
def train_batch(batch_id):
|
|
125
|
-
try:
|
|
126
|
-
trainer = ListTrainer(self.chatbot)
|
|
127
|
-
trainer.train([
|
|
128
|
-
f'Training question {batch_id}',
|
|
129
|
-
f'Training answer {batch_id}',
|
|
130
|
-
])
|
|
131
|
-
except Exception as e:
|
|
132
|
-
errors.append(e)
|
|
133
|
-
|
|
134
|
-
threads = []
|
|
135
|
-
for i in range(20):
|
|
136
|
-
t = threading.Thread(target=train_batch, args=(i,))
|
|
137
|
-
threads.append(t)
|
|
138
|
-
t.start()
|
|
139
|
-
|
|
140
|
-
for t in threads:
|
|
141
|
-
t.join(timeout=10)
|
|
142
|
-
|
|
143
|
-
self.assertEqual(len(errors), 0,
|
|
144
|
-
f"Errors during concurrent training: {errors}")
|
|
145
|
-
|
|
146
|
-
def test_session_cleanup_on_exception(self):
|
|
147
|
-
"""
|
|
148
|
-
Test that sessions are cleaned up even when exceptions occur.
|
|
149
|
-
"""
|
|
150
|
-
# Force an error during a database operation
|
|
151
|
-
try:
|
|
152
|
-
# Create a statement with invalid data
|
|
153
|
-
self.chatbot.storage.create(
|
|
154
|
-
text='', # Empty text might cause issues
|
|
155
|
-
in_response_to=None
|
|
156
|
-
)
|
|
157
|
-
except Exception:
|
|
158
|
-
pass # Expected to fail
|
|
159
|
-
|
|
160
|
-
# Verify the pool is still usable
|
|
161
|
-
response = self.chatbot.get_response('Hi')
|
|
162
|
-
self.assertIsNotNone(response)
|
|
163
|
-
|
|
164
|
-
def test_scoped_session_thread_safety(self):
|
|
165
|
-
"""
|
|
166
|
-
Test that scoped_session provides proper thread isolation.
|
|
167
|
-
"""
|
|
168
|
-
results = {}
|
|
169
|
-
|
|
170
|
-
def check_session_isolation(thread_id):
|
|
171
|
-
# Each thread should get its own session
|
|
172
|
-
session1 = self.chatbot.storage.Session()
|
|
173
|
-
time.sleep(0.01) # Small delay to encourage thread interleaving
|
|
174
|
-
session2 = self.chatbot.storage.Session()
|
|
175
|
-
|
|
176
|
-
# In the same thread, scoped_session should return the same session
|
|
177
|
-
results[thread_id] = (id(session1) == id(session2))
|
|
178
|
-
|
|
179
|
-
session1.close()
|
|
180
|
-
# After close, scoped_session should return the same instance
|
|
181
|
-
# (it doesn't create a new one, just reuses the thread-local one)
|
|
182
|
-
|
|
183
|
-
threads = []
|
|
184
|
-
for i in range(5):
|
|
185
|
-
t = threading.Thread(target=check_session_isolation, args=(i,))
|
|
186
|
-
threads.append(t)
|
|
187
|
-
t.start()
|
|
188
|
-
|
|
189
|
-
for t in threads:
|
|
190
|
-
t.join()
|
|
191
|
-
|
|
192
|
-
# All threads should have gotten consistent session behavior
|
|
193
|
-
for thread_id, same_session in results.items():
|
|
194
|
-
self.assertTrue(same_session,
|
|
195
|
-
f"Thread {thread_id} got different sessions")
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
class ConnectionPoolConfigTestCase(unittest.TestCase):
|
|
199
|
-
"""
|
|
200
|
-
Test cases for connection pool configuration options.
|
|
201
|
-
Note: These tests are skipped for SQLite since it uses SingletonThreadPool.
|
|
202
|
-
"""
|
|
203
|
-
|
|
204
|
-
def test_pool_config_not_applied_to_sqlite(self):
|
|
205
|
-
"""
|
|
206
|
-
Test that pool config is not applied to SQLite (uses SingletonThreadPool).
|
|
207
|
-
"""
|
|
208
|
-
chatbot = ChatBot(
|
|
209
|
-
'SQLiteBot',
|
|
210
|
-
database_uri='sqlite://',
|
|
211
|
-
pool_size=3, # Should be ignored
|
|
212
|
-
max_overflow=2, # Should be ignored
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
# SQLite uses SingletonThreadPool, not QueuePool
|
|
216
|
-
from sqlalchemy.pool import SingletonThreadPool
|
|
217
|
-
self.assertIsInstance(chatbot.storage.engine.pool, SingletonThreadPool)
|
|
218
|
-
|
|
219
|
-
chatbot.storage.close()
|
|
220
|
-
|
|
221
|
-
@unittest.skip("Requires PostgreSQL/MySQL database for testing")
|
|
222
|
-
def test_custom_pool_size_postgres(self):
|
|
223
|
-
"""
|
|
224
|
-
Test that custom pool_size is respected for PostgreSQL.
|
|
225
|
-
"""
|
|
226
|
-
# This test would require a PostgreSQL connection
|
|
227
|
-
# chatbot = ChatBot(
|
|
228
|
-
# 'ConfigBot',
|
|
229
|
-
# database_uri='postgresql://user:pass@localhost/test',
|
|
230
|
-
# pool_size=3,
|
|
231
|
-
# max_overflow=2,
|
|
232
|
-
# )
|
|
233
|
-
# self.assertEqual(chatbot.storage.engine.pool.size(), 3)
|
|
234
|
-
# chatbot.storage.close()
|
|
235
|
-
pass
|
|
236
|
-
|
|
237
|
-
@unittest.skip("Requires PostgreSQL/MySQL database for testing")
|
|
238
|
-
def test_default_pool_config_postgres(self):
|
|
239
|
-
"""
|
|
240
|
-
Test that default pool configuration is applied for PostgreSQL.
|
|
241
|
-
"""
|
|
242
|
-
# This test would require a PostgreSQL connection
|
|
243
|
-
# chatbot = ChatBot(
|
|
244
|
-
# 'DefaultBot',
|
|
245
|
-
# database_uri='postgresql://user:pass@localhost/test',
|
|
246
|
-
# )
|
|
247
|
-
# pool = chatbot.storage.engine.pool
|
|
248
|
-
# self.assertEqual(pool.size(), 10) # Default pool_size
|
|
249
|
-
# chatbot.storage.close()
|
|
250
|
-
pass
|
|
251
|
-
|
|
252
|
-
@unittest.skip("Requires PostgreSQL/MySQL database for testing")
|
|
253
|
-
def test_pool_pre_ping_enabled_postgres(self):
|
|
254
|
-
"""
|
|
255
|
-
Test that pool_pre_ping is enabled by default for PostgreSQL.
|
|
256
|
-
"""
|
|
257
|
-
# This test would require a PostgreSQL connection
|
|
258
|
-
# chatbot = ChatBot(
|
|
259
|
-
# 'PingBot',
|
|
260
|
-
# database_uri='postgresql://user:pass@localhost/test',
|
|
261
|
-
# )
|
|
262
|
-
# self.assertTrue(chatbot.storage.engine.pool._pre_ping)
|
|
263
|
-
# chatbot.storage.close()
|
|
264
|
-
pass
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if __name__ == '__main__':
|
|
268
|
-
unittest.main()
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Proof-of-Concept test for the connection pool exhaustion vulnerability (CVE-TBD).
|
|
3
|
-
|
|
4
|
-
This test demonstrates that the original vulnerability has been fixed.
|
|
5
|
-
The PoC from the security report would cause timeout errors before the fix.
|
|
6
|
-
"""
|
|
7
|
-
import threading
|
|
8
|
-
import tempfile
|
|
9
|
-
import os
|
|
10
|
-
import unittest
|
|
11
|
-
from chatterbot import ChatBot
|
|
12
|
-
from chatterbot.trainers import ListTrainer
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class PoC_VulnerabilityTestCase(unittest.TestCase):
|
|
16
|
-
"""
|
|
17
|
-
Test case demonstrating the fix for the connection pool exhaustion vulnerability.
|
|
18
|
-
|
|
19
|
-
This uses a file-based SQLite database which doesn't have the same
|
|
20
|
-
thread-restrictions as in-memory databases.
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
def setUp(self):
|
|
24
|
-
"""
|
|
25
|
-
Set up test fixtures.
|
|
26
|
-
"""
|
|
27
|
-
# Create a temporary database file
|
|
28
|
-
self.db_fd, self.db_path = tempfile.mkstemp(suffix='.sqlite3')
|
|
29
|
-
|
|
30
|
-
self.chatbot = ChatBot(
|
|
31
|
-
'TestBot',
|
|
32
|
-
database_uri=f'sqlite:///{self.db_path}',
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
# Train with basic data
|
|
36
|
-
trainer = ListTrainer(self.chatbot)
|
|
37
|
-
trainer.train(['hello', 'hi there'])
|
|
38
|
-
|
|
39
|
-
def tearDown(self):
|
|
40
|
-
"""
|
|
41
|
-
Clean up after test.
|
|
42
|
-
"""
|
|
43
|
-
self.chatbot.storage.close()
|
|
44
|
-
os.close(self.db_fd)
|
|
45
|
-
os.unlink(self.db_path)
|
|
46
|
-
|
|
47
|
-
def test_original_poc_no_longer_causes_timeout(self):
|
|
48
|
-
"""
|
|
49
|
-
Test that the original PoC from the security report no longer causes errors.
|
|
50
|
-
|
|
51
|
-
Before the fix: This would cause SQLAlchemy TimeoutError due to pool exhaustion
|
|
52
|
-
After the fix: All requests complete successfully
|
|
53
|
-
"""
|
|
54
|
-
def attack():
|
|
55
|
-
try:
|
|
56
|
-
response = self.chatbot.get_response("hello")
|
|
57
|
-
results.append(('success', str(response)))
|
|
58
|
-
except Exception as e:
|
|
59
|
-
results.append(('error', str(e)))
|
|
60
|
-
|
|
61
|
-
results = []
|
|
62
|
-
threads = []
|
|
63
|
-
|
|
64
|
-
# Original PoC used 30 threads
|
|
65
|
-
for _ in range(30):
|
|
66
|
-
t = threading.Thread(target=attack)
|
|
67
|
-
t.start()
|
|
68
|
-
threads.append(t)
|
|
69
|
-
|
|
70
|
-
for t in threads:
|
|
71
|
-
t.join(timeout=15) # Should complete well before timeout
|
|
72
|
-
|
|
73
|
-
# Count successes and errors
|
|
74
|
-
successes = [r for r in results if r[0] == 'success']
|
|
75
|
-
errors = [r for r in results if r[0] == 'error']
|
|
76
|
-
|
|
77
|
-
# Before fix: Would have many TimeoutError exceptions
|
|
78
|
-
# After fix: All should succeed
|
|
79
|
-
self.assertEqual(len(errors), 0,
|
|
80
|
-
f"Got {len(errors)} errors (expected 0). Errors: {[e[1] for e in errors][:5]}")
|
|
81
|
-
self.assertEqual(len(successes), 30,
|
|
82
|
-
f"Got {len(successes)} successes (expected 30)")
|
|
83
|
-
|
|
84
|
-
def test_high_concurrency_sustained(self):
|
|
85
|
-
"""
|
|
86
|
-
Test sustained high concurrency doesn't cause issues.
|
|
87
|
-
"""
|
|
88
|
-
request_count = 0
|
|
89
|
-
lock = threading.Lock()
|
|
90
|
-
|
|
91
|
-
def make_many_requests():
|
|
92
|
-
nonlocal request_count
|
|
93
|
-
for _ in range(10):
|
|
94
|
-
try:
|
|
95
|
-
self.chatbot.get_response("hello")
|
|
96
|
-
with lock:
|
|
97
|
-
request_count += 1
|
|
98
|
-
except Exception:
|
|
99
|
-
pass
|
|
100
|
-
|
|
101
|
-
threads = []
|
|
102
|
-
for _ in range(10): # 10 threads × 10 requests = 100 total
|
|
103
|
-
t = threading.Thread(target=make_many_requests)
|
|
104
|
-
t.start()
|
|
105
|
-
threads.append(t)
|
|
106
|
-
|
|
107
|
-
for t in threads:
|
|
108
|
-
t.join(timeout=30)
|
|
109
|
-
|
|
110
|
-
# Should have completed all 100 requests
|
|
111
|
-
self.assertGreater(request_count, 90, # Allow for some timing issues
|
|
112
|
-
f"Only completed {request_count}/100 requests")
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
class SequentialPerformanceTestCase(unittest.TestCase):
|
|
116
|
-
"""
|
|
117
|
-
Test that the fixes don't negatively impact single-threaded performance.
|
|
118
|
-
"""
|
|
119
|
-
|
|
120
|
-
def setUp(self):
|
|
121
|
-
"""
|
|
122
|
-
Set up test fixtures.
|
|
123
|
-
"""
|
|
124
|
-
self.db_fd, self.db_path = tempfile.mkstemp(suffix='.sqlite3')
|
|
125
|
-
|
|
126
|
-
self.chatbot = ChatBot(
|
|
127
|
-
'PerfBot',
|
|
128
|
-
database_uri=f'sqlite:///{self.db_path}',
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
trainer = ListTrainer(self.chatbot)
|
|
132
|
-
trainer.train(['hello', 'hi', 'how are you', 'good'])
|
|
133
|
-
|
|
134
|
-
def tearDown(self):
|
|
135
|
-
"""
|
|
136
|
-
Clean up.
|
|
137
|
-
"""
|
|
138
|
-
self.chatbot.storage.close()
|
|
139
|
-
os.close(self.db_fd)
|
|
140
|
-
os.unlink(self.db_path)
|
|
141
|
-
|
|
142
|
-
def test_sequential_requests_still_work(self):
|
|
143
|
-
"""
|
|
144
|
-
Test that normal sequential usage still works correctly.
|
|
145
|
-
"""
|
|
146
|
-
for i in range(50):
|
|
147
|
-
response = self.chatbot.get_response(f"message {i}")
|
|
148
|
-
self.assertIsNotNone(response)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if __name__ == '__main__':
|
|
152
|
-
unittest.main()
|
|
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
|
{chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/0009_tags.py
RENAMED
|
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
|
{chatterbot-1.2.11 → chatterbot-1.2.12}/chatterbot/ext/django_chatterbot/migrations/__init__.py
RENAMED
|
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
|