Skip to content

Commit d36e471

Browse files
authored
Experimental LLM support (gunthercox#2407)
* Add experimental LLM support via Ollama * Add documentation * Better support streaming response * Use streaming response in example * Update documentation and examples * Note design considerations * Update diagram * Update intersphinx links * Add designated classes for LLM clients
1 parent c5ca615 commit d36e471

15 files changed

Lines changed: 438 additions & 7 deletions

chatterbot/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from .chatterbot import ChatBot
55

66

7-
__version__ = '1.2.6'
7+
__version__ = '1.2.7'
88

99
__all__ = (
1010
'ChatBot',

chatterbot/chatterbot.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,21 @@ class ChatBot(object):
4040
4141
:param logger: A ``Logger`` object.
4242
:type logger: logging.Logger
43+
44+
:param model: A definition used to load a large language model.
45+
Defaults to ``None``.
46+
(Added in version 1.2.7)
47+
:type model: dict
48+
49+
:param stream: Return output as a streaming responses when a ``model`` is defined.
50+
(Added in version 1.2.7)
4351
"""
4452

45-
def __init__(self, name, **kwargs):
53+
def __init__(self, name, stream=False, **kwargs):
4654
self.name = name
4755

56+
self.stream = stream
57+
4858
self.logger = kwargs.get('logger', logging.getLogger(__name__))
4959

5060
storage_adapter = kwargs.get('storage_adapter', 'chatterbot.storage.SQLStorageAdapter')
@@ -112,6 +122,11 @@ def __init__(self, name, **kwargs):
112122
# NOTE: 'xx' is the language code for a multi-language model
113123
self.nlp = spacy.blank(self.tagger.language.ISO_639_1)
114124

125+
self.model = None
126+
if model := kwargs.get('model'):
127+
import_path = model.pop('client')
128+
self.model = utils.initialize_class(import_path, self, **model)
129+
115130
# Allow the bot to save input it receives so that it can learn
116131
self.read_only = kwargs.get('read_only', False)
117132

@@ -185,6 +200,10 @@ def get_response(self, statement=None, **kwargs) -> Statement:
185200
additional_response_selection_parameters
186201
)
187202

203+
# If streaming is enabled return the response immediately
204+
if self.stream:
205+
return response
206+
188207
# Update any response data that needs to be changed
189208
if persist_values_to_response:
190209
for response_key in persist_values_to_response:
@@ -219,6 +238,12 @@ def generate_response(self, input_statement, additional_response_selection_param
219238
result = None
220239
max_confidence = -1
221240

241+
# If a model is provided, use it to process the input statement
242+
# instead of the logic adapters
243+
if self.model:
244+
model_response = self.model.process(input_statement)
245+
return model_response
246+
222247
for adapter in self.logic_adapters:
223248
if adapter.can_process(input_statement):
224249

chatterbot/llm.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""
2+
Large Language Model (LLM) clients.
3+
4+
.. note::
5+
As a part of the development process when choosing models it is
6+
important to research and understand the models you are using.
7+
8+
A good example of why this is important can be found in the
9+
description of the Phi-3 model from Microsoft which discusses
10+
responsible AI considerations such as the limitations of the
11+
model, and appropriate use cases.
12+
https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf#responsible-ai-considerations
13+
"""
14+
15+
16+
class ModelClient:
17+
"""
18+
A base class to define the interface for language model clients.
19+
"""
20+
21+
def __init__(self, chatbot, model: str, **kwargs):
22+
self.chatbot = chatbot
23+
self.model = model
24+
25+
26+
class Ollama(ModelClient):
27+
"""
28+
This client class allows the use of Ollama models for chatbot responses.
29+
30+
.. warning::
31+
This is a new and experimental class. It may not work as expected
32+
and its functionality may change in future releases.
33+
34+
.. note::
35+
Added in version 1.2.7
36+
"""
37+
38+
def __init__(self, chatbot, model: str, **kwargs):
39+
"""
40+
keyword arguments:
41+
host: The host URL for the Ollama server.
42+
Default is 'http://localhost:11434'.
43+
"""
44+
super().__init__(chatbot, model)
45+
from ollama import Client, AsyncClient
46+
47+
self.host = kwargs.get('host', 'http://localhost:11434')
48+
49+
# TODO: Look into supporting the async client
50+
self.async_mode = False
51+
52+
# https://github.com/ollama/ollama-python
53+
if self.async_mode:
54+
self.client = AsyncClient(
55+
host=self.host,
56+
)
57+
else:
58+
self.client = Client(
59+
host=self.host,
60+
)
61+
62+
def process(self, statement):
63+
64+
system_message = {
65+
'role': 'system',
66+
'content': 'Please keep responses short and concise.'
67+
}
68+
message = {
69+
'role': 'user',
70+
'content': statement.text
71+
}
72+
73+
if self.chatbot.stream:
74+
for part in self.client.chat(
75+
model=self.model,
76+
messages=[system_message, message],
77+
stream=True
78+
):
79+
yield part['message']['content']
80+
else:
81+
response = self.client.chat(
82+
model=self.model,
83+
messages=[system_message, message]
84+
)
85+
86+
return response.message.content
87+
88+
89+
class OpenAI(ModelClient):
90+
"""
91+
This client class allows the use of the OpenAI API to generate chatbot responses.
92+
93+
.. warning::
94+
This is a new and experimental class. It may not work as expected
95+
and its functionality may change in future releases.
96+
97+
.. note::
98+
Added in version 1.2.7
99+
"""
100+
101+
def __init__(self, chatbot, model, **kwargs):
102+
super().__init__(chatbot, model, **kwargs)
103+
from openai import OpenAI as OpenAIClient
104+
from openai import AsyncOpenAI as AsyncOpenAIClient
105+
106+
self.host = kwargs.get('host', None)
107+
108+
# TODO: Look into supporting the async client
109+
self.async_mode = False
110+
111+
# https://github.com/openai/openai-python
112+
if self.async_mode:
113+
self.client = AsyncOpenAIClient(
114+
base_url=self.host,
115+
)
116+
else:
117+
self.client = OpenAIClient(
118+
base_url=self.host,
119+
)
120+
121+
def process(self, statement):
122+
123+
system_message = {
124+
'role': 'developer',
125+
'content': 'Please keep responses short and concise.'
126+
}
127+
message = {
128+
'role': 'user',
129+
'content': statement.text
130+
}
131+
132+
if self.chatbot.stream:
133+
for part in self.client.chat.completions.create(
134+
model=self.model,
135+
messages=[system_message, message],
136+
stream=True
137+
):
138+
yield part
139+
else:
140+
response = self.client.chat.completions.create(
141+
model=self.model,
142+
messages=[system_message, message]
143+
)
144+
145+
return response.output_text

chatterbot/logic/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from chatterbot.logic.time_adapter import TimeLogicAdapter
66
from chatterbot.logic.unit_conversion import UnitConversion
77

8+
89
__all__ = (
910
'LogicAdapter',
1011
'BestMatch',

chatterbot/utils.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
"""
22
ChatterBot utility functions
33
"""
4+
from typing import Union
45
import importlib
56
import time
67

78

8-
def import_module(dotted_path):
9+
def import_module(dotted_path: str):
910
"""
1011
Imports the specified module based on the
1112
dot notated import path for the module.
@@ -17,7 +18,7 @@ def import_module(dotted_path):
1718
return getattr(module, module_parts[-1])
1819

1920

20-
def initialize_class(data, *args, **kwargs):
21+
def initialize_class(data: Union[dict, str], *args, **kwargs):
2122
"""
2223
:param data: A string or dictionary containing a import_path attribute.
2324
"""

docs/_static/dialog-processing-flow-llm.svg

Lines changed: 1 addition & 0 deletions
Loading

docs/_static/dialog-processing-flow.svg

Lines changed: 1 addition & 1 deletion
Loading

docs/_static/style.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
table {
2+
width: 100%;
3+
}
4+
5+
th.head p {
6+
text-align: center;
7+
}
8+
9+
.table-justified td {
10+
width: 50%;
11+
}
12+
13+
.caption-text {
14+
padding-top: 3px;
15+
padding-bottom: 3px;
16+
}
17+
118
.wy-side-nav-search {
219
background-color: #300a24;
320
}

docs/conf.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,5 +220,6 @@
220220

221221
# Configuration for intersphinx
222222
intersphinx_mapping = {
223-
'python': ('https://docs.python.org/3', None)
223+
'python': ('https://docs.python.org/3', None),
224+
'mathparse': ('https://mathparse.chatterbot.us/', None),
224225
}

docs/glossary.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Glossary
1313
logic adapter
1414
An adapter class that allows a ChatBot instance to select a response to
1515

16+
RAG
17+
Retrieval-Augmented Generation. A method of by which a large language model
18+
can retrieve information from a database or other source of information.
19+
1620
storage adapter
1721
A class that allows a chat bot to store information somewhere, such as a database.
1822

@@ -22,6 +26,15 @@ Glossary
2226
and hypothesis testing, checking occurrences or validating linguistic
2327
rules within a specific language territory [2]_.
2428

29+
large language models
30+
A type of artificial intelligence model that can generate generate
31+
human-like text, often trained on a significantly large corpus of
32+
text data.
33+
34+
MCP
35+
Model Context Protocol. A protocol for providing context data to a large
36+
language model to enable or improve its ability to perform various tasks.
37+
2538
preprocessors
2639
A member of a list of functions that can be used to modify text
2740
input that the chat bot receives before the text is passed to

0 commit comments

Comments
 (0)