ChatterBot 1.2.4__py3-none-any.whl → 1.2.6__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.
chatterbot/__init__.py CHANGED
@@ -4,7 +4,7 @@ ChatterBot is a machine learning, conversational dialog engine.
4
4
  from .chatterbot import ChatBot
5
5
 
6
6
 
7
- __version__ = '1.2.4'
7
+ __version__ = '1.2.6'
8
8
 
9
9
  __all__ = (
10
10
  'ChatBot',
chatterbot/chatterbot.py CHANGED
@@ -3,6 +3,7 @@ from chatterbot.storage import StorageAdapter
3
3
  from chatterbot.logic import LogicAdapter
4
4
  from chatterbot.search import TextSearch, IndexedTextSearch
5
5
  from chatterbot.tagging import PosLemmaTagger
6
+ from chatterbot.conversation import Statement
6
7
  from chatterbot import languages
7
8
  from chatterbot import utils
8
9
  import spacy
@@ -114,13 +115,12 @@ class ChatBot(object):
114
115
  # Allow the bot to save input it receives so that it can learn
115
116
  self.read_only = kwargs.get('read_only', False)
116
117
 
117
- def get_response(self, statement=None, **kwargs):
118
+ def get_response(self, statement=None, **kwargs) -> Statement:
118
119
  """
119
120
  Return the bot's response based on the input.
120
121
 
121
122
  :param statement: An statement object or string.
122
123
  :returns: A response to the input.
123
- :rtype: Statement
124
124
 
125
125
  :param additional_response_selection_parameters: Parameters to pass to the
126
126
  chat bot's logic adapters to control response selection.
@@ -314,7 +314,7 @@ class ChatBot(object):
314
314
  # Save the response statement
315
315
  return self.storage.create(**statement.serialize())
316
316
 
317
- def get_latest_response(self, conversation):
317
+ def get_latest_response(self, conversation: str):
318
318
  """
319
319
  Returns the latest response in a conversation if it exists.
320
320
  Returns None if a matching conversation cannot be found.
chatterbot/comparisons.py CHANGED
@@ -19,19 +19,17 @@ class Comparator:
19
19
  def __call__(self, statement_a, statement_b):
20
20
  return self.compare(statement_a, statement_b)
21
21
 
22
- def compare_text(self, text_a, text_b):
22
+ def compare_text(self, text_a: str, text_b: str) -> float:
23
23
  """
24
24
  Implemented in subclasses: compare text_a to text_b.
25
25
 
26
26
  :return: The percent of similarity between the statements based on the implemented algorithm.
27
- :rtype: float
28
27
  """
29
28
  return 0
30
29
 
31
- def compare(self, statement_a, statement_b):
30
+ def compare(self, statement_a, statement_b) -> float:
32
31
  """
33
32
  :return: The percent of similarity between the statements based on the implemented algorithm.
34
- :rtype: float
35
33
  """
36
34
  return self.compare_text(statement_a.text, statement_b.text)
37
35
 
@@ -46,12 +44,11 @@ class LevenshteinDistance(Comparator):
46
44
  based on the Levenshtein distance algorithm.
47
45
  """
48
46
 
49
- def compare_text(self, text_a, text_b):
47
+ def compare_text(self, text_a: str, text_b: str) -> float:
50
48
  """
51
49
  Compare the two pieces of text.
52
50
 
53
51
  :return: The percent of similarity between the text of the statements.
54
- :rtype: float
55
52
  """
56
53
 
57
54
  # Return 0 if either statement has a None text value
@@ -105,12 +102,11 @@ class SpacySimilarity(Comparator):
105
102
  # Disable the Named Entity Recognition (NER) component because it is not necessary
106
103
  self.nlp = spacy.load(model, exclude=['ner'])
107
104
 
108
- def compare_text(self, text_a, text_b):
105
+ def compare_text(self, text_a: str, text_b: str) -> float:
109
106
  """
110
107
  Compare the similarity of two strings.
111
108
 
112
109
  :return: The percent of similarity between the closest synset distance.
113
- :rtype: float
114
110
  """
115
111
 
116
112
  # Return 0 if either statement has a None text value
@@ -157,7 +153,7 @@ class JaccardSimilarity(Comparator):
157
153
  # Disable the Named Entity Recognition (NER) component because it is not necessary
158
154
  self.nlp = spacy.load(model, exclude=['ner'])
159
155
 
160
- def compare_text(self, text_a, text_b):
156
+ def compare_text(self, text_a: str, text_b: str) -> float:
161
157
  """
162
158
  Return the calculated similarity of two
163
159
  statements based on the Jaccard index.
@@ -40,10 +40,9 @@ class StatementMixin(object):
40
40
  """
41
41
  self.tags.extend(tags)
42
42
 
43
- def serialize(self):
43
+ def serialize(self) -> dict:
44
44
  """
45
45
  :returns: A dictionary representation of the statement object.
46
- :rtype: dict
47
46
  """
48
47
  data = {}
49
48
 
@@ -32,7 +32,8 @@ class AbstractBaseTag(models.Model):
32
32
 
33
33
  name = models.SlugField(
34
34
  max_length=constants.TAG_NAME_MAX_LENGTH,
35
- unique=True
35
+ unique=True,
36
+ help_text='The unique name of the tag.'
36
37
  )
37
38
 
38
39
  class Meta:
@@ -49,16 +50,19 @@ class AbstractBaseStatement(models.Model, StatementMixin):
49
50
  """
50
51
 
51
52
  text = models.CharField(
52
- max_length=constants.STATEMENT_TEXT_MAX_LENGTH
53
+ max_length=constants.STATEMENT_TEXT_MAX_LENGTH,
54
+ help_text='The text of the statement.'
53
55
  )
54
56
 
55
57
  search_text = models.CharField(
56
58
  max_length=constants.STATEMENT_TEXT_MAX_LENGTH,
57
- blank=True
59
+ blank=True,
60
+ help_text='A modified version of the statement text optimized for searching.'
58
61
  )
59
62
 
60
63
  conversation = models.CharField(
61
- max_length=constants.CONVERSATION_LABEL_MAX_LENGTH
64
+ max_length=constants.CONVERSATION_LABEL_MAX_LENGTH,
65
+ help_text='A label used to link this statement to a conversation.'
62
66
  )
63
67
 
64
68
  created_at = models.DateTimeField(
@@ -68,21 +72,25 @@ class AbstractBaseStatement(models.Model, StatementMixin):
68
72
 
69
73
  in_response_to = models.CharField(
70
74
  max_length=constants.STATEMENT_TEXT_MAX_LENGTH,
71
- null=True
75
+ null=True,
76
+ help_text='The text of the statement that this statement is in response to.'
72
77
  )
73
78
 
74
79
  search_in_response_to = models.CharField(
75
80
  max_length=constants.STATEMENT_TEXT_MAX_LENGTH,
76
- blank=True
81
+ blank=True,
82
+ help_text='A modified version of the in_response_to text optimized for searching.'
77
83
  )
78
84
 
79
85
  persona = models.CharField(
80
- max_length=constants.PERSONA_MAX_LENGTH
86
+ max_length=constants.PERSONA_MAX_LENGTH,
87
+ help_text='A label used to link this statement to a persona.'
81
88
  )
82
89
 
83
90
  tags = models.ManyToManyField(
84
91
  TAG_MODEL,
85
- related_name='statements'
92
+ related_name='statements',
93
+ help_text='The tags that are associated with this statement.'
86
94
  )
87
95
 
88
96
  # This is the confidence with which the chat bot believes
@@ -6,3 +6,10 @@ class DjangoChatterBotConfig(AppConfig):
6
6
  name = 'chatterbot.ext.django_chatterbot'
7
7
  label = 'django_chatterbot'
8
8
  verbose_name = 'Django ChatterBot'
9
+
10
+ def ready(self):
11
+ from chatterbot.ext.django_chatterbot import settings as defaults
12
+ from django.conf import settings
13
+
14
+ settings.CHATTERBOT = getattr(settings, 'CHATTERBOT', {})
15
+ settings.CHATTERBOT.update(defaults.CHATTERBOT_DEFAULTS)
@@ -0,0 +1,53 @@
1
+ # Generated by Django 4.1 on 2025-03-29 23:27
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('django_chatterbot', '0019_alter_statement_id_alter_tag_id_and_more'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='statement',
15
+ name='conversation',
16
+ field=models.CharField(help_text='A label used to link this statement to a conversation.', max_length=32),
17
+ ),
18
+ migrations.AlterField(
19
+ model_name='statement',
20
+ name='in_response_to',
21
+ field=models.CharField(help_text='The text of the statement that this statement is in response to.', max_length=255, null=True),
22
+ ),
23
+ migrations.AlterField(
24
+ model_name='statement',
25
+ name='persona',
26
+ field=models.CharField(help_text='A label used to link this statement to a persona.', max_length=50),
27
+ ),
28
+ migrations.AlterField(
29
+ model_name='statement',
30
+ name='search_in_response_to',
31
+ field=models.CharField(blank=True, help_text='A modified version of the in_response_to text optimized for searching.', max_length=255),
32
+ ),
33
+ migrations.AlterField(
34
+ model_name='statement',
35
+ name='search_text',
36
+ field=models.CharField(blank=True, help_text='A modified version of the statement text optimized for searching.', max_length=255),
37
+ ),
38
+ migrations.AlterField(
39
+ model_name='statement',
40
+ name='tags',
41
+ field=models.ManyToManyField(help_text='The tags that are associated with this statement.', related_name='statements', to='django_chatterbot.tag'),
42
+ ),
43
+ migrations.AlterField(
44
+ model_name='statement',
45
+ name='text',
46
+ field=models.CharField(help_text='The text of the statement.', max_length=255),
47
+ ),
48
+ migrations.AlterField(
49
+ model_name='tag',
50
+ name='name',
51
+ field=models.SlugField(help_text='The unique name of the tag.', unique=True),
52
+ ),
53
+ ]
@@ -5,7 +5,7 @@ from django.conf import settings
5
5
  from chatterbot import constants
6
6
 
7
7
 
8
- CHATTERBOT_SETTINGS = getattr(settings, 'CHATTERBOT', {})
8
+ CHATTERBOT = getattr(settings, 'CHATTERBOT', {})
9
9
 
10
10
  CHATTERBOT_DEFAULTS = {
11
11
  'name': 'ChatterBot',
@@ -13,5 +13,4 @@ CHATTERBOT_DEFAULTS = {
13
13
  'django_app_name': constants.DEFAULT_DJANGO_APP_NAME
14
14
  }
15
15
 
16
- CHATTERBOT = CHATTERBOT_DEFAULTS.copy()
17
- CHATTERBOT.update(CHATTERBOT_SETTINGS)
16
+ CHATTERBOT.update(CHATTERBOT_DEFAULTS)
@@ -4,6 +4,7 @@ from chatterbot.adapters import Adapter
4
4
  from chatterbot.storage import StorageAdapter
5
5
  from chatterbot.search import IndexedTextSearch
6
6
  from chatterbot.conversation import Statement
7
+ from chatterbot import utils
7
8
 
8
9
 
9
10
  class LogicAdapter(Adapter):
@@ -28,7 +29,7 @@ class LogicAdapter(Adapter):
28
29
  :type response_selection_method: collections.abc.Callable
29
30
 
30
31
  :param default_response:
31
- The default response returned by this logic adaper
32
+ The default response returned by this logic adapter
32
33
  if there is no other possible response to return.
33
34
  :type default_response: str or list or tuple
34
35
  """
@@ -50,6 +51,14 @@ class LogicAdapter(Adapter):
50
51
  'maximum_similarity_threshold', 0.95
51
52
  )
52
53
 
54
+ if response_selection_method := kwargs.get('response_selection_method'):
55
+ if isinstance(response_selection_method, str):
56
+ # If an import path is provided, import the method
57
+ response_selection_method = utils.import_module(
58
+ response_selection_method
59
+ )
60
+ kwargs['response_selection_method'] = response_selection_method
61
+
53
62
  # By default, select the first available response
54
63
  self.select_response = kwargs.get(
55
64
  'response_selection_method',
@@ -68,18 +77,16 @@ class LogicAdapter(Adapter):
68
77
  Statement(text=default) for default in default_responses
69
78
  ]
70
79
 
71
- def can_process(self, statement):
80
+ def can_process(self, statement) -> bool:
72
81
  """
73
82
  A preliminary check that is called to determine if a
74
83
  logic adapter can process a given statement. By default,
75
84
  this method returns true but it can be overridden in
76
85
  child classes as needed.
77
-
78
- :rtype: bool
79
86
  """
80
87
  return True
81
88
 
82
- def process(self, statement, additional_response_selection_parameters=None):
89
+ def process(self, statement, additional_response_selection_parameters=None) -> Statement:
83
90
  """
84
91
  Override this method and implement your logic for selecting a response to an input statement.
85
92
 
@@ -97,12 +104,10 @@ class LogicAdapter(Adapter):
97
104
  :param additional_response_selection_parameters: Parameters to be used when
98
105
  filtering results to choose a response from.
99
106
  :type additional_response_selection_parameters: dict
100
-
101
- :rtype: Statement
102
107
  """
103
108
  raise self.AdapterMethodNotImplementedError()
104
109
 
105
- def get_default_response(self, input_statement):
110
+ def get_default_response(self, input_statement: Statement) -> Statement:
106
111
  """
107
112
  This method is called when a logic adapter is unable to generate any
108
113
  other meaningful response.
@@ -125,7 +130,7 @@ class LogicAdapter(Adapter):
125
130
  return response
126
131
 
127
132
  @property
128
- def class_name(self):
133
+ def class_name(self) -> str:
129
134
  """
130
135
  Return the name of the current logic adapter class.
131
136
  This is typically used for logging and debugging.
@@ -1,12 +1,13 @@
1
1
  """
2
2
  Statement pre-processors.
3
3
  """
4
+ from chatterbot.conversation import Statement
4
5
  from unicodedata import normalize
5
6
  from re import sub as re_sub
6
7
  from html import unescape
7
8
 
8
9
 
9
- def clean_whitespace(statement):
10
+ def clean_whitespace(statement: Statement) -> Statement:
10
11
  """
11
12
  Remove any consecutive whitespace characters from the statement text.
12
13
  """
@@ -24,7 +25,7 @@ def clean_whitespace(statement):
24
25
  return statement
25
26
 
26
27
 
27
- def unescape_html(statement):
28
+ def unescape_html(statement: Statement) -> Statement:
28
29
  """
29
30
  Convert escaped html characters into unescaped html characters.
30
31
  For example: "&lt;b&gt;" becomes "<b>".
@@ -34,7 +35,7 @@ def unescape_html(statement):
34
35
  return statement
35
36
 
36
37
 
37
- def convert_to_ascii(statement):
38
+ def convert_to_ascii(statement: Statement) -> Statement:
38
39
  """
39
40
  Converts unicode characters to ASCII character equivalents.
40
41
  For example: "på fédéral" becomes "pa federal".
@@ -2,10 +2,11 @@
2
2
  Response selection methods determines which response should be used in
3
3
  the event that multiple responses are generated within a logic adapter.
4
4
  """
5
+ from chatterbot.conversation import Statement
5
6
  import logging
6
7
 
7
8
 
8
- def get_most_frequent_response(input_statement, response_list, storage=None):
9
+ def get_most_frequent_response(input_statement, response_list, storage=None) -> Statement:
9
10
  """
10
11
  :param input_statement: A statement, that closely matches an input to the chat bot.
11
12
  :type input_statement: Statement
@@ -18,7 +19,6 @@ def get_most_frequent_response(input_statement, response_list, storage=None):
18
19
  :type storage: StorageAdapter
19
20
 
20
21
  :return: The response statement with the greatest number of occurrences.
21
- :rtype: Statement
22
22
  """
23
23
  matching_response = None
24
24
  occurrence_count = -1
@@ -41,7 +41,7 @@ def get_most_frequent_response(input_statement, response_list, storage=None):
41
41
  return matching_response
42
42
 
43
43
 
44
- def get_first_response(input_statement, response_list, storage=None):
44
+ def get_first_response(input_statement, response_list, storage=None) -> Statement:
45
45
  """
46
46
  :param input_statement: A statement, that closely matches an input to the chat bot.
47
47
  :type input_statement: Statement
@@ -54,7 +54,6 @@ def get_first_response(input_statement, response_list, storage=None):
54
54
  :type storage: StorageAdapter
55
55
 
56
56
  :return: Return the first statement in the response list.
57
- :rtype: Statement
58
57
  """
59
58
  logger = logging.getLogger(__name__)
60
59
  logger.info('Selecting first response from list of {} options.'.format(
@@ -63,7 +62,7 @@ def get_first_response(input_statement, response_list, storage=None):
63
62
  return response_list[0]
64
63
 
65
64
 
66
- def get_random_response(input_statement, response_list, storage=None):
65
+ def get_random_response(input_statement, response_list, storage=None) -> Statement:
67
66
  """
68
67
  :param input_statement: A statement, that closely matches an input to the chat bot.
69
68
  :type input_statement: Statement
@@ -76,7 +75,6 @@ def get_random_response(input_statement, response_list, storage=None):
76
75
  :type storage: StorageAdapter
77
76
 
78
77
  :return: Choose a random response from the selection.
79
- :rtype: Statement
80
78
  """
81
79
  from random import choice
82
80
  logger = logging.getLogger(__name__)
@@ -110,6 +110,9 @@ class DjangoStorageAdapter(StorageAdapter):
110
110
 
111
111
  tags = kwargs.pop('tags', [])
112
112
 
113
+ if 'search_in_response_to' in kwargs and kwargs['search_in_response_to'] is None:
114
+ kwargs['search_in_response_to'] = ''
115
+
113
116
  statement = Statement(**kwargs)
114
117
 
115
118
  statement.save()
@@ -169,7 +172,7 @@ class DjangoStorageAdapter(StorageAdapter):
169
172
  search_text=statement.search_text,
170
173
  conversation=statement.conversation,
171
174
  in_response_to=statement.in_response_to,
172
- search_in_response_to=statement.search_in_response_to,
175
+ search_in_response_to=statement.search_in_response_to or '',
173
176
  created_at=statement.created_at
174
177
  )
175
178
 
@@ -44,7 +44,7 @@ class SQLStorageAdapter(StorageAdapter):
44
44
  dbapi_connection.execute('PRAGMA journal_mode=WAL')
45
45
  dbapi_connection.execute('PRAGMA synchronous=NORMAL')
46
46
 
47
- if not inspect(self.engine).has_table(self.engine, 'statement'):
47
+ if not inspect(self.engine).has_table('statement'):
48
48
  self.create_database()
49
49
 
50
50
  # Check if the expected index exists on the text field of the statement table
chatterbot/tagging.py CHANGED
@@ -1,3 +1,4 @@
1
+ from typing import List, Union, Tuple
1
2
  from chatterbot import languages
2
3
  from chatterbot.utils import get_model_for_language
3
4
  import spacy
@@ -20,7 +21,7 @@ class LowercaseTagger(object):
20
21
  'chatterbot_lowercase_indexer', name='chatterbot_lowercase_indexer', last=True
21
22
  )
22
23
 
23
- def get_text_index_string(self, text):
24
+ def get_text_index_string(self, text: Union[str, List[str]]):
24
25
  if isinstance(text, list):
25
26
  documents = self.nlp.pipe(text)
26
27
  return [document._.search_index for document in documents]
@@ -28,7 +29,7 @@ class LowercaseTagger(object):
28
29
  document = self.nlp(text)
29
30
  return document._.search_index
30
31
 
31
- def as_nlp_pipeline(self, texts):
32
+ def as_nlp_pipeline(self, texts: Union[List[str], Tuple[str, dict]]):
32
33
 
33
34
  process_as_tuples = texts and isinstance(texts[0], tuple)
34
35
 
@@ -52,7 +53,7 @@ class PosLemmaTagger(object):
52
53
  'chatterbot_bigram_indexer', name='chatterbot_bigram_indexer', last=True
53
54
  )
54
55
 
55
- def get_text_index_string(self, text):
56
+ def get_text_index_string(self, text: Union[str, List[str]]):
56
57
  """
57
58
  Return a string of text containing part-of-speech, lemma pairs.
58
59
  """
@@ -63,7 +64,7 @@ class PosLemmaTagger(object):
63
64
  document = self.nlp(text)
64
65
  return document._.search_index
65
66
 
66
- def as_nlp_pipeline(self, texts):
67
+ def as_nlp_pipeline(self, texts: Union[List[str], Tuple[str, dict]]):
67
68
  """
68
69
  Accepts a single string or a list of strings, or a list of tuples
69
70
  where the first element is the text and the second element is a
chatterbot/trainers.py CHANGED
@@ -1,12 +1,13 @@
1
1
  import os
2
- import sys
3
2
  import csv
4
3
  import time
5
4
  import glob
6
5
  import json
7
6
  import tarfile
7
+ from typing import List, Union
8
8
  from tqdm import tqdm
9
9
  from dateutil import parser as date_parser
10
+ from chatterbot.chatterbot import ChatBot
10
11
  from chatterbot.conversation import Statement
11
12
 
12
13
 
@@ -20,7 +21,7 @@ class Trainer(object):
20
21
  the environment variable if it is set.
21
22
  """
22
23
 
23
- def __init__(self, chatbot, **kwargs):
24
+ def __init__(self, chatbot: ChatBot, **kwargs):
24
25
  self.chatbot = chatbot
25
26
 
26
27
  environment_default = bool(int(os.environ.get('CHATTERBOT_SHOW_TRAINING_PROGRESS', True)))
@@ -30,7 +31,7 @@ class Trainer(object):
30
31
  environment_default
31
32
  )
32
33
 
33
- def get_preprocessed_statement(self, input_statement):
34
+ def get_preprocessed_statement(self, input_statement: Statement) -> Statement:
34
35
  """
35
36
  Preprocess the input statement.
36
37
  """
@@ -58,7 +59,7 @@ class Trainer(object):
58
59
  )
59
60
  super().__init__(message or default)
60
61
 
61
- def _generate_export_data(self):
62
+ def _generate_export_data(self) -> list:
62
63
  result = []
63
64
  for statement in self.chatbot.storage.filter():
64
65
  if statement.in_response_to:
@@ -82,7 +83,7 @@ class ListTrainer(Trainer):
82
83
  where the list represents a conversation.
83
84
  """
84
85
 
85
- def train(self, conversation: list):
86
+ def train(self, conversation: List[str]):
86
87
  """
87
88
  Train the chat bot based on the provided list of
88
89
  statements that represents a single conversation.
@@ -95,7 +96,6 @@ class ListTrainer(Trainer):
95
96
  # Run the pipeline in bulk to improve performance
96
97
  documents = self.chatbot.tagger.as_nlp_pipeline(conversation)
97
98
 
98
- # for text in enumerate(conversation):
99
99
  for document in tqdm(documents, desc='List Trainer', disable=self.disable_progress):
100
100
  statement_search_text = document._.search_index
101
101
 
@@ -123,7 +123,7 @@ class ChatterBotCorpusTrainer(Trainer):
123
123
  ChatterBot dialog corpus.
124
124
  """
125
125
 
126
- def train(self, *corpus_paths):
126
+ def train(self, *corpus_paths: Union[str, List[str]]):
127
127
  from chatterbot.corpus import load_corpus, list_corpus_files
128
128
 
129
129
  data_file_paths = []
@@ -178,7 +178,17 @@ class GenericFileTrainer(Trainer):
178
178
  or directory of those file types.
179
179
  """
180
180
 
181
- def __init__(self, chatbot, **kwargs):
181
+ # NOTE: If the value is an integer, this be the
182
+ # column index instead of the key or header
183
+ DEFAULT_STATEMENT_TO_HEADER_MAPPING = {
184
+ 'text': 'text',
185
+ 'conversation': 'conversation',
186
+ 'created_at': 'created_at',
187
+ 'persona': 'persona',
188
+ 'tags': 'tags'
189
+ }
190
+
191
+ def __init__(self, chatbot: ChatBot, **kwargs):
182
192
  """
183
193
  data_path: str The path to the data file or directory.
184
194
  field_map: dict A dictionary containing the column name to header mapping.
@@ -187,22 +197,12 @@ class GenericFileTrainer(Trainer):
187
197
 
188
198
  self.file_extension = None
189
199
 
190
- # NOTE: If the key is an integer, this be the
191
- # column index instead of the key or header
192
- DEFAULT_STATEMENT_TO_HEADER_MAPPING = {
193
- 'text': 'text',
194
- 'conversation': 'conversation',
195
- 'created_at': 'created_at',
196
- 'persona': 'persona',
197
- 'tags': 'tags'
198
- }
199
-
200
200
  self.field_map = kwargs.get(
201
201
  'field_map',
202
- DEFAULT_STATEMENT_TO_HEADER_MAPPING
202
+ self.DEFAULT_STATEMENT_TO_HEADER_MAPPING
203
203
  )
204
204
 
205
- def _get_file_list(self, data_path, limit):
205
+ def _get_file_list(self, data_path: str, limit: Union[int, None]):
206
206
  """
207
207
  Get a list of files to read from the data set.
208
208
  """
@@ -210,7 +210,7 @@ class GenericFileTrainer(Trainer):
210
210
  if self.file_extension is None:
211
211
  raise self.TrainerInitializationException(
212
212
  'The file_extension attribute must be set before calling train().'
213
- )
213
+ )
214
214
 
215
215
  # List all csv or json files in the specified directory
216
216
  if os.path.isdir(data_path):
@@ -226,7 +226,7 @@ class GenericFileTrainer(Trainer):
226
226
 
227
227
  yield file_path
228
228
  else:
229
- return [data_path]
229
+ yield data_path
230
230
 
231
231
  def train(self, data_path: str, limit=None):
232
232
  """
@@ -254,7 +254,9 @@ class GenericFileTrainer(Trainer):
254
254
 
255
255
  statements_to_create = []
256
256
 
257
- with open(data_file, 'r', encoding='utf-8') as file:
257
+ file_abspath = os.path.abspath(data_file)
258
+
259
+ with open(file_abspath, 'r', encoding='utf-8') as file:
258
260
 
259
261
  if self.file_extension == 'json':
260
262
  data = json.load(file)
@@ -281,18 +283,39 @@ class GenericFileTrainer(Trainer):
281
283
 
282
284
  text_row = self.field_map['text']
283
285
 
284
- documents = self.chatbot.tagger.as_nlp_pipeline([
286
+ try:
287
+ documents = self.chatbot.tagger.as_nlp_pipeline([
288
+ (
289
+ row[text_row],
290
+ {
291
+ # Include any defined metadata columns
292
+ key: row[value]
293
+ for key, value in self.field_map.items()
294
+ if key != text_row
295
+ }
296
+ ) for row in data if len(row) > 0
297
+ ])
298
+ except KeyError as e:
299
+ raise KeyError(
300
+ f'{e}. Please check the field_map parameter used to initialize '
301
+ f'the training class and remove this value if it is not needed. '
302
+ f'Current mapping: {self.field_map}'
303
+ )
304
+
305
+ response_to_search_index_mapping = {}
306
+
307
+ if 'in_response_to' in self.field_map.keys():
308
+ # Generate the search_in_response_to value for the in_response_to fields
309
+ response_documents = self.chatbot.tagger.as_nlp_pipeline([
285
310
  (
286
- row[text_row],
287
- {
288
- # Include any defined metadata columns
289
- key: row[value]
290
- for key, value in self.field_map.items()
291
- if key != text_row
292
- }
293
- ) for row in data if len(row) > 0
311
+ row[self.field_map['in_response_to']]
312
+ ) for row in data if len(row) > 0 and row[self.field_map['in_response_to']] is not None
294
313
  ])
295
314
 
315
+ # (Process the response values the same way as the text values)
316
+ for document in response_documents:
317
+ response_to_search_index_mapping[document.text] = document._.search_index
318
+
296
319
  for document, context in documents:
297
320
  statement = Statement(
298
321
  text=document.text,
@@ -305,14 +328,19 @@ class GenericFileTrainer(Trainer):
305
328
  statement.created_at = date_parser.parse(context['created_at'])
306
329
 
307
330
  statement.search_text = document._.search_index
308
- statement.search_in_response_to = previous_statement_search_text
309
331
 
310
332
  # Use the in_response_to attribute for the previous statement if
311
333
  # one is defined, otherwise use the last statement which was created
312
334
  if 'in_response_to' in self.field_map.keys():
313
335
  statement.in_response_to = context.get(self.field_map['in_response_to'], None)
336
+ statement.search_in_response_to = response_to_search_index_mapping.get(
337
+ context.get(self.field_map['in_response_to'], None), ''
338
+ )
314
339
  else:
340
+ # List-type data such as CSVs with no response specified can use
341
+ # the previous statement as the in_response_to value
315
342
  statement.in_response_to = previous_statement_text
343
+ statement.search_in_response_to = previous_statement_search_text
316
344
 
317
345
  for preprocessor in self.chatbot.preprocessors:
318
346
  statement = preprocessor(statement)
@@ -336,7 +364,6 @@ class GenericFileTrainer(Trainer):
336
364
  )
337
365
  )
338
366
 
339
-
340
367
  class CsvFileTrainer(GenericFileTrainer):
341
368
  """
342
369
  .. note::
@@ -349,11 +376,11 @@ class CsvFileTrainer(GenericFileTrainer):
349
376
  parameter is set to 'tsv'.
350
377
 
351
378
  :param str file_extension: The file extension to look for when searching for files (defaults to 'csv').
352
- :param str field_map: A dictionary containing the database column name to header mapping.
379
+ :param dict field_map: A dictionary containing the database column name to header mapping.
353
380
  Values can be either the header name (str) or the column index (int).
354
381
  """
355
382
 
356
- def __init__(self, chatbot, **kwargs):
383
+ def __init__(self, chatbot: ChatBot, **kwargs):
357
384
  super().__init__(chatbot, **kwargs)
358
385
 
359
386
  self.file_extension = kwargs.get('file_extension', 'csv')
@@ -367,26 +394,26 @@ class JsonFileTrainer(GenericFileTrainer):
367
394
  Allow chatbots to be trained with data from a JSON file or
368
395
  directory of JSON files.
369
396
 
370
- :param str field_map: A dictionary containing the database column name to header mapping.
397
+ :param dict field_map: A dictionary containing the database column name to header mapping.
371
398
  """
372
399
 
373
- def __init__(self, chatbot, **kwargs):
400
+ DEFAULT_STATEMENT_TO_KEY_MAPPING = {
401
+ 'text': 'text',
402
+ 'conversation': 'conversation',
403
+ 'created_at': 'created_at',
404
+ 'in_response_to': 'in_response_to',
405
+ 'persona': 'persona',
406
+ 'tags': 'tags'
407
+ }
408
+
409
+ def __init__(self, chatbot: ChatBot, **kwargs):
374
410
  super().__init__(chatbot, **kwargs)
375
411
 
376
412
  self.file_extension = 'json'
377
413
 
378
- DEFAULT_STATEMENT_TO_KEY_MAPPING = {
379
- 'text': 'text',
380
- 'conversation': 'conversation',
381
- 'created_at': 'created_at',
382
- 'in_response_to': 'in_response_to',
383
- 'persona': 'persona',
384
- 'tags': 'tags'
385
- }
386
-
387
414
  self.field_map = kwargs.get(
388
415
  'field_map',
389
- DEFAULT_STATEMENT_TO_KEY_MAPPING
416
+ self.DEFAULT_STATEMENT_TO_KEY_MAPPING
390
417
  )
391
418
 
392
419
 
@@ -403,7 +430,7 @@ class UbuntuCorpusTrainer(CsvFileTrainer):
403
430
  :param str ubuntu_corpus_data_directory: The directory where the Ubuntu corpus data is already located, or where it should be downloaded and extracted.
404
431
  """
405
432
 
406
- def __init__(self, chatbot, **kwargs):
433
+ def __init__(self, chatbot: ChatBot, **kwargs):
407
434
  super().__init__(chatbot, **kwargs)
408
435
  home_directory = os.path.expanduser('~')
409
436
 
@@ -425,7 +452,7 @@ class UbuntuCorpusTrainer(CsvFileTrainer):
425
452
  'persona': 1,
426
453
  }
427
454
 
428
- def is_downloaded(self, file_path):
455
+ def is_downloaded(self, file_path: str):
429
456
  """
430
457
  Check if the data file is already downloaded.
431
458
  """
@@ -435,7 +462,7 @@ class UbuntuCorpusTrainer(CsvFileTrainer):
435
462
 
436
463
  return False
437
464
 
438
- def is_extracted(self, file_path):
465
+ def is_extracted(self, file_path: str):
439
466
  """
440
467
  Check if the data file is already extracted.
441
468
  """
@@ -445,7 +472,7 @@ class UbuntuCorpusTrainer(CsvFileTrainer):
445
472
  return True
446
473
  return False
447
474
 
448
- def download(self, url, show_status=True):
475
+ def download(self, url: str, show_status=True):
449
476
  """
450
477
  Download a file from the given url.
451
478
  Show a progress indicator for the download status.
@@ -484,7 +511,7 @@ class UbuntuCorpusTrainer(CsvFileTrainer):
484
511
  print('Download location: %s' % file_path)
485
512
  return file_path
486
513
 
487
- def extract(self, file_path):
514
+ def extract(self, file_path: str):
488
515
  """
489
516
  Extract a tar file at the specified file path.
490
517
  """
@@ -524,7 +551,7 @@ class UbuntuCorpusTrainer(CsvFileTrainer):
524
551
 
525
552
  return True
526
553
 
527
- def _get_file_list(self, data_path, limit):
554
+ def _get_file_list(self, data_path: str, limit: Union[int, None]):
528
555
  """
529
556
  Get a list of files to read from the data set.
530
557
  """
@@ -555,7 +582,7 @@ class UbuntuCorpusTrainer(CsvFileTrainer):
555
582
 
556
583
  yield file_path
557
584
 
558
- def train(self, data_download_url, limit=None):
585
+ def train(self, data_download_url: str, limit: Union[int, None] = None):
559
586
  """
560
587
  :param str data_download_url: The URL to download the Ubuntu dialog corpus from.
561
588
  :param int limit: The maximum number of files to train from.
chatterbot/utils.py CHANGED
@@ -3,7 +3,6 @@ ChatterBot utility functions
3
3
  """
4
4
  import importlib
5
5
  import time
6
- import sys
7
6
 
8
7
 
9
8
  def import_module(dotted_path):
@@ -71,7 +70,7 @@ def validate_adapter_class(validate_class, adapter_class):
71
70
  )
72
71
 
73
72
 
74
- def get_response_time(chatbot, statement='Hello'):
73
+ def get_response_time(chatbot, statement='Hello') -> float:
75
74
  """
76
75
  Returns the amount of time taken for a given
77
76
  chat bot to return a response.
@@ -80,7 +79,6 @@ def get_response_time(chatbot, statement='Hello'):
80
79
  :type chatbot: ChatBot
81
80
 
82
81
  :returns: The response time in seconds.
83
- :rtype: float
84
82
  """
85
83
  start_time = time.time()
86
84
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ChatterBot
3
- Version: 1.2.4
3
+ Version: 1.2.6
4
4
  Summary: ChatterBot is a machine learning, conversational dialog engine
5
5
  Author: Gunther Cox
6
6
  License-Expression: BSD-3-Clause
@@ -66,10 +66,11 @@ known conversations. The language independent design of ChatterBot allows it
66
66
  to be trained to speak any language.
67
67
 
68
68
  [![Package Version](https://img.shields.io/pypi/v/chatterbot.svg)](https://pypi.python.org/pypi/chatterbot/)
69
- [![Python 3.9](https://img.shields.io/badge/python-3.9-blue.svg)](https://www.python.org/downloads/release/python-360/)
69
+ [![Python 3.12](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/downloads/release/python-360/)
70
70
  [![Coverage Status](https://img.shields.io/coveralls/gunthercox/ChatterBot.svg)](https://coveralls.io/r/gunthercox/ChatterBot)
71
- [![Code Climate](https://codeclimate.com/github/gunthercox/ChatterBot/badges/gpa.svg)](https://codeclimate.com/github/gunthercox/ChatterBot)
71
+ [![Follow on Bluesky](https://img.shields.io/badge/🦋%20Bluesky-1185fe)](https://bsky.app/profile/chatterbot.us)
72
72
  [![Join the chat at https://gitter.im/chatterbot/Lobby](https://badges.gitter.im/chatterbot/Lobby.svg)](https://gitter.im/chatterbot/Lobby?utm_source=badge&utm_medium=badge&utm_content=badge)
73
+ <!-- [![Code Climate](https://codeclimate.com/github/gunthercox/ChatterBot/badges/gpa.svg)](https://codeclimate.com/github/gunthercox/ChatterBot) -->
73
74
 
74
75
  An example of typical input would be something like this:
75
76
 
@@ -1,31 +1,31 @@
1
- chatterbot/__init__.py,sha256=Woq2bFnaAs8yTE2HVPsxXEyzFrXs1njsGJnJVgbYGvI,158
1
+ chatterbot/__init__.py,sha256=wHoKxLsCOplHJ02JmxRCg5cRL-Z-blQblhJKdE95Pck,158
2
2
  chatterbot/__main__.py,sha256=zvH4uxtGlGrP-ht_LkhX29duzjm3hRH800SDCq4YOwg,637
3
3
  chatterbot/adapters.py,sha256=LJ_KqLpHKPdYAFpMGK63RVH4weV5X0Zh5uGyan6qdVU,878
4
- chatterbot/chatterbot.py,sha256=BW_XQK78iOvc0fZ8EsEglNUdjyRE2lxUI_sP-fa4gCc,12505
5
- chatterbot/comparisons.py,sha256=8kkjW-lhS-57XSUlQI5B-dAdJO-CvkIirWLBKtbe4gw,6187
4
+ chatterbot/chatterbot.py,sha256=nqxdeTBWdA_LDIEWTMf2gphvpNfd0c9htNwrxa_7pzo,12543
5
+ chatterbot/comparisons.py,sha256=kYYR5DUipdzr7ok08m8s6r9LBez4GBQICaSmLZcmoiM,6184
6
6
  chatterbot/components.py,sha256=ld3Xam8olBClvE5QqcFYggE7Q7tODCFek7BO7lhfyeU,1782
7
7
  chatterbot/constants.py,sha256=c_KPQKc82CHX6H3maeyTYqWatx6j-N-8HJhmejoVi60,1875
8
- chatterbot/conversation.py,sha256=Y-WOxPN7I3igRyAEe5py1sfS6JIYPdbwjVlY3kM8Ys8,3175
8
+ chatterbot/conversation.py,sha256=-JshXr4Uvsl8NVeEgm1am2e4UStBN1BXfMQHYkQFvco,3162
9
9
  chatterbot/corpus.py,sha256=n1sFU0KF-oRdj7g3nXft7uHOVilwbj_mVZnaeIHgC54,2409
10
10
  chatterbot/exceptions.py,sha256=gqAlckDna6SB53kyqutmCtCqWxia40WBdUOnGVQ2Kbk,163
11
11
  chatterbot/filters.py,sha256=vDSDJz2FM10xT6ybs7qJiqy4X5I4gTEfwEnjBGUxZ9g,847
12
12
  chatterbot/languages.py,sha256=XSenfc5FxHk_JWG5gGHsZvjvrPBbCaVCm_OU-BeER_M,32784
13
13
  chatterbot/parsing.py,sha256=vS-w70cMkjq4YEpDOv_pXWhAI6Zj06WYDAcMDhYDj0M,23174
14
- chatterbot/preprocessors.py,sha256=aI4v987dZc7GOKhO43i0i73EX748hehYSpzikFHpEXs,1271
15
- chatterbot/response_selection.py,sha256=aYeZ54jpGIcQnI-1-TDcua_f1p3PiM5_iMg4hF5ZaIU,2951
14
+ chatterbot/preprocessors.py,sha256=kqsgnejSj6Z1rr9U2TGHKOp-MMaFWBdNT41EwyhQFls,1389
15
+ chatterbot/response_selection.py,sha256=JpUVuBYrgxhHkDMRHXyWvhluSLxQED5mAhE1-VvJSmg,2970
16
16
  chatterbot/search.py,sha256=FTwwON2eKPWqoc5uoKh4AUmuXDCqyfMcMcXB4wijpxg,4910
17
- chatterbot/tagging.py,sha256=czcI2g18vILujphkjvobRyEewJU8-QjS7QRzY-hCZ4o,2429
18
- chatterbot/trainers.py,sha256=9mxi1_UmtiuXuEzpn4uztnV8PObD0Xt0PrAbTZ6oyt0,19294
19
- chatterbot/utils.py,sha256=tGmUt-KYYylD2fiG_oq_XxhGbAHukzwudZ_6hNuraIA,2944
17
+ chatterbot/tagging.py,sha256=si0PQ3CY5EbiZ0-PIslbBtExZaQZ3NYrLUGoourweKo,2585
18
+ chatterbot/trainers.py,sha256=CZezNX68Byg9gg2z-PUZbTc5pqzTzbhgSmqHx7P6Ivg,20973
19
+ chatterbot/utils.py,sha256=ubPiBapvUvdFVhrDjxqq5IGekUh9qMUJs_dQ605xLAI,2924
20
20
  chatterbot/vectorstores.py,sha256=-S1NB8PrZzoFIu95n2W7N4UaXuCUpyDUXIGYFebjv08,2056
21
21
  chatterbot/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  chatterbot/ext/django_chatterbot/__init__.py,sha256=iWzmBzpAsYwkwi1faxAPFY9L1bbL97RgVXK2uqULIMc,92
23
- chatterbot/ext/django_chatterbot/abstract_models.py,sha256=PkuBGS0uv2toL3jGE1U6HJLCLXoKwpRNohm9JbVT_y4,3303
23
+ chatterbot/ext/django_chatterbot/abstract_models.py,sha256=skMqOqxIL_xOyfU8ZabtDqzUKTz0YtqGkaylB3TC1JA,3883
24
24
  chatterbot/ext/django_chatterbot/admin.py,sha256=jyCJGLRcRoeaoYWZFv-TCivcDUNTE4Li7pmYE8_hYCM,266
25
- chatterbot/ext/django_chatterbot/apps.py,sha256=Gsk3wEv2raklK79D8eKXXEY1OMMjGHnFIFSdvnaMgzo,195
25
+ chatterbot/ext/django_chatterbot/apps.py,sha256=YSqF77pD9Pl2-BP6KeQZRr22XhL8gInpHVkVTLtZsUU,464
26
26
  chatterbot/ext/django_chatterbot/model_admin.py,sha256=aJVE6u1fquMrvFTm_0aclK6FSAcUtfZcj-RPnXmm6iM,353
27
27
  chatterbot/ext/django_chatterbot/models.py,sha256=GqIXvDxSRKaeFgAVh71t9MRS6qEAIOUQ8a42I4wBxYs,360
28
- chatterbot/ext/django_chatterbot/settings.py,sha256=JH59sMoX06yC346VCA7W1unQkiNnHE2ryAMgEiYWogo,430
28
+ chatterbot/ext/django_chatterbot/settings.py,sha256=CBPnTLCIrznPm_YHd0-ss8E_gzRrydujBX37gI0G0gA,381
29
29
  chatterbot/ext/django_chatterbot/migrations/0001_initial.py,sha256=mZQYculJOOVyo9QdSlnbT3Bp_B5_e7y81aEyRQjfRgQ,1266
30
30
  chatterbot/ext/django_chatterbot/migrations/0002_statement_extra_data.py,sha256=hbMubk-eBsseP8QZ5pxt13jrxP5e5RJyEFOqxxk5Tzk,441
31
31
  chatterbot/ext/django_chatterbot/migrations/0003_change_occurrence_default.py,sha256=maDz1Y9F5oTeCFotQdk7Tf-hpkjWVUqiDVXP4nQJTNg,408
@@ -45,24 +45,25 @@ chatterbot/ext/django_chatterbot/migrations/0016_statement_stemmed_text.py,sha25
45
45
  chatterbot/ext/django_chatterbot/migrations/0017_tags_unique.py,sha256=3RGCsV9fAFAYjuyHbGVUXFdzvT3fj9FAKoNZmyfaR9M,692
46
46
  chatterbot/ext/django_chatterbot/migrations/0018_text_max_length.py,sha256=508TxWE8xGff1jKExxxBj9Ulu9Exz6rWDHnNxFli2xU,876
47
47
  chatterbot/ext/django_chatterbot/migrations/0019_alter_statement_id_alter_tag_id_and_more.py,sha256=rsVxwDFMQ-cU1KMhjDq9Wcl_6gTPKc_dc3p-gv_R7v8,999
48
+ chatterbot/ext/django_chatterbot/migrations/0020_alter_statement_conversation_and_more.py,sha256=PpcIvl6sO9jNBxW4UgvFMv_j0eNmUBHire5qo7u-XAg,2133
48
49
  chatterbot/ext/django_chatterbot/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
50
  chatterbot/ext/sqlalchemy_app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
51
  chatterbot/ext/sqlalchemy_app/models.py,sha256=ZQ-R_5rA-f1agaqYGUQhuuO7zx__BvTDUvJo5R7ZrDY,2492
51
52
  chatterbot/logic/__init__.py,sha256=28-5swBCPfSVMl8xB5C8frOKZ2oj28rQfenbd9E4r-4,531
52
53
  chatterbot/logic/best_match.py,sha256=8TNW0uZ_Uq-XPfaZUMUZDVH6KzDT65j59xblxQBv-dQ,4820
53
- chatterbot/logic/logic_adapter.py,sha256=5kNEirh5fiF5hhSMFXD7bIkKwXHmrSsSS4qDm-6xry0,4694
54
+ chatterbot/logic/logic_adapter.py,sha256=zcYrzyywZ0kT3GsjcMRr69abvswurlNYOPLsUpIwaB8,5144
54
55
  chatterbot/logic/mathematical_evaluation.py,sha256=GPDKUwNFajERof2R-MkPGi2jJRP-rKAGm_f0V9JHDHE,2282
55
56
  chatterbot/logic/specific_response.py,sha256=akWHkfe0AjzlCUvjs_PbKFNkX4SZhu_tzY45xCRXoo0,2236
56
57
  chatterbot/logic/time_adapter.py,sha256=1PT6tWtGauZLRH02-Xlh2LublDpu_3hnCqHBqNGM9yg,2256
57
58
  chatterbot/logic/unit_conversion.py,sha256=-ENMLqZqtZx0riUi0guda2oJECST0M7pZG4cSIv3ieM,5898
58
59
  chatterbot/storage/__init__.py,sha256=ADw0WQe0YKr1UIDQLaxwf0mHDnuKW_CSzgz11K4TM-4,465
59
- chatterbot/storage/django_storage.py,sha256=S5S4GipD7FyNJy4RWu5-S8sLPuSJIObwTtqTpnJu-ok,6159
60
+ chatterbot/storage/django_storage.py,sha256=BpuVEO4rPOiPu7f7KW1Zyar2LqEXy6I4HgPYhyGP0kE,6305
60
61
  chatterbot/storage/mongodb.py,sha256=Ozvdvcjb3LGZxcvbSQGzwP9VloYQbmsa2FaKunFpMyU,7934
61
62
  chatterbot/storage/redis.py,sha256=FKROrzZ-7WXZ8ZoK0dKmTDdS45TxL04XOSeu0p3Jrak,12675
62
- chatterbot/storage/sql_storage.py,sha256=dAMLByFKQgbiTFoBUtKDeqadYRdwVO5fz1OONTcVCH4,13076
63
+ chatterbot/storage/sql_storage.py,sha256=wESsp0OKuXjYmAZ7dl-ztX7lt4xEpjD0WCBQXK22__4,13063
63
64
  chatterbot/storage/storage_adapter.py,sha256=fvyb-qNiB0HMJ0siVMCWUIY--6d-C47N1_kKZVFZAv4,6110
64
- chatterbot-1.2.4.dist-info/licenses/LICENSE,sha256=5b04U8mi0wp5gJMYlKi49EalnD9Q2nwY_6UEI_Avgu4,1476
65
- chatterbot-1.2.4.dist-info/METADATA,sha256=lrGa5gxvPrNRh6fCKqr7zPvRp_qmY293ijj0ODW4uZM,7049
66
- chatterbot-1.2.4.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
67
- chatterbot-1.2.4.dist-info/top_level.txt,sha256=W2TzAbAJ-eBXTIKZZhVlkrh87msJNmBQpyhkrHqjSrE,11
68
- chatterbot-1.2.4.dist-info/RECORD,,
65
+ chatterbot-1.2.6.dist-info/licenses/LICENSE,sha256=5b04U8mi0wp5gJMYlKi49EalnD9Q2nwY_6UEI_Avgu4,1476
66
+ chatterbot-1.2.6.dist-info/METADATA,sha256=lOBXOb2GEaOguy3_Rwqo_TEP77GeunbhiI9BGqB1zfc,7175
67
+ chatterbot-1.2.6.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
68
+ chatterbot-1.2.6.dist-info/top_level.txt,sha256=W2TzAbAJ-eBXTIKZZhVlkrh87msJNmBQpyhkrHqjSrE,11
69
+ chatterbot-1.2.6.dist-info/RECORD,,