ollama-chat 0.9.0__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.
- ollama_chat-0.9.0/LICENSE +21 -0
- ollama_chat-0.9.0/PKG-INFO +89 -0
- ollama_chat-0.9.0/README.md +64 -0
- ollama_chat-0.9.0/pyproject.toml +2 -0
- ollama_chat-0.9.0/setup.cfg +46 -0
- ollama_chat-0.9.0/src/ollama_chat/__init__.py +0 -0
- ollama_chat-0.9.0/src/ollama_chat/__main__.py +12 -0
- ollama_chat-0.9.0/src/ollama_chat/app.py +309 -0
- ollama_chat-0.9.0/src/ollama_chat/main.py +58 -0
- ollama_chat-0.9.0/src/ollama_chat/ollama.py +79 -0
- ollama_chat-0.9.0/src/ollama_chat/static/ollamaChat.smd +214 -0
- ollama_chat-0.9.0/src/ollama_chat.egg-info/PKG-INFO +89 -0
- ollama_chat-0.9.0/src/ollama_chat.egg-info/SOURCES.txt +16 -0
- ollama_chat-0.9.0/src/ollama_chat.egg-info/dependency_links.txt +1 -0
- ollama_chat-0.9.0/src/ollama_chat.egg-info/entry_points.txt +2 -0
- ollama_chat-0.9.0/src/ollama_chat.egg-info/requires.txt +3 -0
- ollama_chat-0.9.0/src/ollama_chat.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Craig A. Hobbs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ollama-chat
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: ollama-chat
|
|
5
|
+
Home-page: https://github.com/craigahobbs/ollama-chat
|
|
6
|
+
Author: Craig A. Hobbs
|
|
7
|
+
Author-email: craigahobbs@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: ollama-chat
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: chisel>=1.2.7
|
|
23
|
+
Requires-Dist: ollama>=0.1.9
|
|
24
|
+
Requires-Dist: waitress>=3.0.0
|
|
25
|
+
|
|
26
|
+
# ollama-chat
|
|
27
|
+
|
|
28
|
+
[](https://pypi.org/project/ollama-chat/)
|
|
29
|
+
[](https://pypi.org/project/ollama-chat/)
|
|
30
|
+
[](https://github.com/craigahobbs/ollama-chat/blob/main/LICENSE)
|
|
31
|
+
[](https://pypi.org/project/ollama-chat/)
|
|
32
|
+
|
|
33
|
+
**Ollama Chat** is a simple yet useful web chat client for
|
|
34
|
+
[Ollama](https://ollama.com)
|
|
35
|
+
that allows you to chat locally (and privately) with
|
|
36
|
+
[open-source LLMs](https://ollama.com/library).
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Installation
|
|
40
|
+
|
|
41
|
+
To get up and running with Ollama Chat follows these steps:
|
|
42
|
+
|
|
43
|
+
1. Install and start [Ollama](https://ollama.com)
|
|
44
|
+
|
|
45
|
+
2. Install Ollama Chat
|
|
46
|
+
|
|
47
|
+
~~~
|
|
48
|
+
pip install ollama-chat
|
|
49
|
+
~~~
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Starting Ollama Chat
|
|
53
|
+
|
|
54
|
+
To start Ollama Chat, open a terminal prompt and run the Ollama Chat application:
|
|
55
|
+
|
|
56
|
+
~~~
|
|
57
|
+
ollama-chat
|
|
58
|
+
~~~
|
|
59
|
+
|
|
60
|
+
A web browser is launched and opens the Ollama Chat web application.
|
|
61
|
+
|
|
62
|
+
By default, a configuration file, "ollama-chat.json", is created in the current directory to save
|
|
63
|
+
your conversations.
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
## Future Features
|
|
67
|
+
|
|
68
|
+
In no particular order...
|
|
69
|
+
|
|
70
|
+
- Save conversation as Markdown file
|
|
71
|
+
|
|
72
|
+
- Conversation title edit
|
|
73
|
+
|
|
74
|
+
- File / Directory / URL text inclusion in prompt
|
|
75
|
+
|
|
76
|
+
- Local model management (pull, rm)
|
|
77
|
+
- [Models JSON](https://huggingface.co/api/models)
|
|
78
|
+
|
|
79
|
+
- Prompt library
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
This package is developed using [python-build](https://github.com/craigahobbs/python-build#readme).
|
|
85
|
+
It was started using [python-template](https://github.com/craigahobbs/python-template#readme) as follows:
|
|
86
|
+
|
|
87
|
+
~~~
|
|
88
|
+
template-specialize python-template/template/ ollama-chat/ -k package ollama-chat -k name 'Craig A. Hobbs' -k email 'craigahobbs@gmail.com' -k github 'craigahobbs' -k noapi 1
|
|
89
|
+
~~~
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# ollama-chat
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/ollama-chat/)
|
|
4
|
+
[](https://pypi.org/project/ollama-chat/)
|
|
5
|
+
[](https://github.com/craigahobbs/ollama-chat/blob/main/LICENSE)
|
|
6
|
+
[](https://pypi.org/project/ollama-chat/)
|
|
7
|
+
|
|
8
|
+
**Ollama Chat** is a simple yet useful web chat client for
|
|
9
|
+
[Ollama](https://ollama.com)
|
|
10
|
+
that allows you to chat locally (and privately) with
|
|
11
|
+
[open-source LLMs](https://ollama.com/library).
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Installation
|
|
15
|
+
|
|
16
|
+
To get up and running with Ollama Chat follows these steps:
|
|
17
|
+
|
|
18
|
+
1. Install and start [Ollama](https://ollama.com)
|
|
19
|
+
|
|
20
|
+
2. Install Ollama Chat
|
|
21
|
+
|
|
22
|
+
~~~
|
|
23
|
+
pip install ollama-chat
|
|
24
|
+
~~~
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Starting Ollama Chat
|
|
28
|
+
|
|
29
|
+
To start Ollama Chat, open a terminal prompt and run the Ollama Chat application:
|
|
30
|
+
|
|
31
|
+
~~~
|
|
32
|
+
ollama-chat
|
|
33
|
+
~~~
|
|
34
|
+
|
|
35
|
+
A web browser is launched and opens the Ollama Chat web application.
|
|
36
|
+
|
|
37
|
+
By default, a configuration file, "ollama-chat.json", is created in the current directory to save
|
|
38
|
+
your conversations.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## Future Features
|
|
42
|
+
|
|
43
|
+
In no particular order...
|
|
44
|
+
|
|
45
|
+
- Save conversation as Markdown file
|
|
46
|
+
|
|
47
|
+
- Conversation title edit
|
|
48
|
+
|
|
49
|
+
- File / Directory / URL text inclusion in prompt
|
|
50
|
+
|
|
51
|
+
- Local model management (pull, rm)
|
|
52
|
+
- [Models JSON](https://huggingface.co/api/models)
|
|
53
|
+
|
|
54
|
+
- Prompt library
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## Development
|
|
58
|
+
|
|
59
|
+
This package is developed using [python-build](https://github.com/craigahobbs/python-build#readme).
|
|
60
|
+
It was started using [python-template](https://github.com/craigahobbs/python-template#readme) as follows:
|
|
61
|
+
|
|
62
|
+
~~~
|
|
63
|
+
template-specialize python-template/template/ ollama-chat/ -k package ollama-chat -k name 'Craig A. Hobbs' -k email 'craigahobbs@gmail.com' -k github 'craigahobbs' -k noapi 1
|
|
64
|
+
~~~
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
name = ollama-chat
|
|
3
|
+
version = 0.9.0
|
|
4
|
+
url = https://github.com/craigahobbs/ollama-chat
|
|
5
|
+
author = Craig A. Hobbs
|
|
6
|
+
author_email = craigahobbs@gmail.com
|
|
7
|
+
license = MIT
|
|
8
|
+
description = ollama-chat
|
|
9
|
+
long_description = file:README.md
|
|
10
|
+
long_description_content_type = text/markdown
|
|
11
|
+
keywords = ollama-chat
|
|
12
|
+
classifiers =
|
|
13
|
+
Development Status :: 5 - Production/Stable
|
|
14
|
+
Intended Audience :: Developers
|
|
15
|
+
License :: OSI Approved :: MIT License
|
|
16
|
+
Operating System :: OS Independent
|
|
17
|
+
Programming Language :: Python :: 3.8
|
|
18
|
+
Programming Language :: Python :: 3.9
|
|
19
|
+
Programming Language :: Python :: 3.10
|
|
20
|
+
Programming Language :: Python :: 3.11
|
|
21
|
+
Programming Language :: Python :: 3.12
|
|
22
|
+
Topic :: Utilities
|
|
23
|
+
|
|
24
|
+
[options]
|
|
25
|
+
packages = ollama_chat
|
|
26
|
+
package_dir =
|
|
27
|
+
= src
|
|
28
|
+
install_requires =
|
|
29
|
+
chisel >= 1.2.7
|
|
30
|
+
ollama >= 0.1.9
|
|
31
|
+
waitress >= 3.0.0
|
|
32
|
+
|
|
33
|
+
[options.entry_points]
|
|
34
|
+
console_scripts =
|
|
35
|
+
ollama-chat = ollama_chat.main:main
|
|
36
|
+
|
|
37
|
+
[options.package_data]
|
|
38
|
+
ollama_chat = \
|
|
39
|
+
static/index.html, \
|
|
40
|
+
static/ollamaChat.mds, \
|
|
41
|
+
static/ollamaChat.smd
|
|
42
|
+
|
|
43
|
+
[egg_info]
|
|
44
|
+
tag_build =
|
|
45
|
+
tag_date = 0
|
|
46
|
+
|
|
File without changes
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# Licensed under the MIT License
|
|
2
|
+
# https://github.com/craigahobbs/ollama-chat/blob/main/LICENSE
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
The ollama-chat back-end application
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
import copy
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import importlib.resources as pkg_resources
|
|
13
|
+
import threading
|
|
14
|
+
import uuid
|
|
15
|
+
|
|
16
|
+
import chisel
|
|
17
|
+
import ollama
|
|
18
|
+
import schema_markdown
|
|
19
|
+
|
|
20
|
+
from .ollama import OllamaChat, config_conversation
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OllamaChatApplication(chisel.Application):
|
|
24
|
+
"""
|
|
25
|
+
The ollama-chat back-end API WSGI application class
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
__slots__ = ('config', 'chats')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def __init__(self, config_path):
|
|
32
|
+
super().__init__()
|
|
33
|
+
self.config = ConfigManager(config_path)
|
|
34
|
+
self.chats = {}
|
|
35
|
+
|
|
36
|
+
# Add the chisel documentation application
|
|
37
|
+
self.add_requests(chisel.create_doc_requests())
|
|
38
|
+
|
|
39
|
+
# Add the APIs
|
|
40
|
+
self.add_request(delete_conversation)
|
|
41
|
+
self.add_request(delete_conversation_exchange)
|
|
42
|
+
self.add_request(get_conversation)
|
|
43
|
+
self.add_request(get_conversations)
|
|
44
|
+
self.add_request(get_model)
|
|
45
|
+
self.add_request(get_models)
|
|
46
|
+
self.add_request(regenerate_conversation_exchange)
|
|
47
|
+
self.add_request(reply_conversation)
|
|
48
|
+
self.add_request(set_model)
|
|
49
|
+
self.add_request(start_conversation)
|
|
50
|
+
self.add_request(stop_conversation)
|
|
51
|
+
|
|
52
|
+
# Add the ollama-chat statics
|
|
53
|
+
self.add_static(
|
|
54
|
+
'index.html',
|
|
55
|
+
'text/html; charset=utf-8',
|
|
56
|
+
(('GET', None), ('GET', '/')),
|
|
57
|
+
'The Ollama Chat application HTML'
|
|
58
|
+
)
|
|
59
|
+
self.add_static(
|
|
60
|
+
'ollamaChat.bare',
|
|
61
|
+
'text/plain; charset=utf-8',
|
|
62
|
+
(('GET', None),),
|
|
63
|
+
'The Ollama Chat application BareScript'
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def add_static(self, filename, content_type, urls, doc):
|
|
68
|
+
with pkg_resources.open_binary('ollama_chat.static', filename) as fh:
|
|
69
|
+
self.add_request(chisel.StaticRequest(
|
|
70
|
+
filename,
|
|
71
|
+
fh.read(),
|
|
72
|
+
content_type=content_type,
|
|
73
|
+
urls=urls,
|
|
74
|
+
doc=doc,
|
|
75
|
+
doc_group='Ollama Chat Statics'
|
|
76
|
+
))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ConfigManager:
|
|
80
|
+
__slots__ = ('config_path', 'config_lock', 'config')
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
DEFAULT_MODEL = 'llama3:latest'
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def __init__(self, config_path):
|
|
87
|
+
self.config_path = config_path
|
|
88
|
+
self.config_lock = threading.Lock()
|
|
89
|
+
|
|
90
|
+
# Ensure the config file exists with default config if it doesn't exist
|
|
91
|
+
if os.path.isfile(config_path):
|
|
92
|
+
with open(config_path, 'r', encoding='utf-8') as fh_config:
|
|
93
|
+
self.config = schema_markdown.validate_type(OLLAMA_CHAT_TYPES, 'OllamaChatConfig', json.loads(fh_config.read()))
|
|
94
|
+
else:
|
|
95
|
+
self.config = {'model': ConfigManager.DEFAULT_MODEL, 'conversations': []}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@contextmanager
|
|
99
|
+
def __call__(self, save=False):
|
|
100
|
+
# Acquire the config lock
|
|
101
|
+
self.config_lock.acquire()
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
# If no model is set, set the default model
|
|
105
|
+
is_saving = save
|
|
106
|
+
if 'model' not in self.config:
|
|
107
|
+
self.config['model'] = ConfigManager.DEFAULT_MODEL
|
|
108
|
+
is_saving = True
|
|
109
|
+
|
|
110
|
+
# Yield the config on context entry
|
|
111
|
+
yield self.config
|
|
112
|
+
|
|
113
|
+
# Save the config file on context exit, if requested
|
|
114
|
+
if is_saving and not self.config.get('noSave'):
|
|
115
|
+
with open(self.config_path, 'w', encoding='utf-8') as fh_config:
|
|
116
|
+
json.dump(self.config, fh_config, indent=4, sort_keys = True)
|
|
117
|
+
finally:
|
|
118
|
+
# Release the config lock
|
|
119
|
+
self.config_lock.release()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# The Ollama Chat type model
|
|
123
|
+
with pkg_resources.open_text('ollama_chat.static', 'ollamaChat.smd') as cm_smd:
|
|
124
|
+
OLLAMA_CHAT_TYPES = schema_markdown.parse_schema_markdown(cm_smd.read())
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@chisel.action(name='getModels', types=OLLAMA_CHAT_TYPES)
|
|
128
|
+
def get_models(unused_ctx, unused_req):
|
|
129
|
+
return {
|
|
130
|
+
'models': [
|
|
131
|
+
{
|
|
132
|
+
'model': model['name'],
|
|
133
|
+
'size': model['size']
|
|
134
|
+
}
|
|
135
|
+
for model in ollama.list()['models']
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@chisel.action(name='getModel', types=OLLAMA_CHAT_TYPES)
|
|
141
|
+
def get_model(ctx, unused_req):
|
|
142
|
+
with ctx.app.config() as config:
|
|
143
|
+
return {'model': config['model']}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@chisel.action(name='setModel', types=OLLAMA_CHAT_TYPES)
|
|
147
|
+
def set_model(ctx, req):
|
|
148
|
+
with ctx.app.config(save=True) as config:
|
|
149
|
+
config['model'] = req['model']
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@chisel.action(name='getConversations', types=OLLAMA_CHAT_TYPES)
|
|
153
|
+
def get_conversations(ctx, unused_req):
|
|
154
|
+
conversations = []
|
|
155
|
+
with ctx.app.config() as config:
|
|
156
|
+
for conversation in config['conversations']:
|
|
157
|
+
info = dict(conversation)
|
|
158
|
+
del info['exchanges']
|
|
159
|
+
info['generating'] = conversation['id'] in ctx.app.chats
|
|
160
|
+
conversations.append(info)
|
|
161
|
+
return {
|
|
162
|
+
'conversations': conversations
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@chisel.action(name='startConversation', types=OLLAMA_CHAT_TYPES)
|
|
167
|
+
def start_conversation(ctx, req):
|
|
168
|
+
with ctx.app.config() as config:
|
|
169
|
+
# Compute the conversation title
|
|
170
|
+
user_prompt = req['user']
|
|
171
|
+
max_title_len = 50
|
|
172
|
+
if len(user_prompt) <= max_title_len:
|
|
173
|
+
title = user_prompt
|
|
174
|
+
else:
|
|
175
|
+
title_suffix = '...'
|
|
176
|
+
title = f'{user_prompt[:max_title_len - len(title_suffix)]}{title_suffix}'
|
|
177
|
+
|
|
178
|
+
# Create the new conversation object
|
|
179
|
+
id_ = str(uuid.uuid4())
|
|
180
|
+
model = config['model']
|
|
181
|
+
conversation = {
|
|
182
|
+
'id': id_,
|
|
183
|
+
'model': model,
|
|
184
|
+
'title': title,
|
|
185
|
+
'exchanges': [
|
|
186
|
+
{
|
|
187
|
+
'user': req['user'],
|
|
188
|
+
'model': ''
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Add the new conversation to the application config
|
|
194
|
+
config['conversations'].insert(0, conversation)
|
|
195
|
+
|
|
196
|
+
# Start the model chat
|
|
197
|
+
ctx.app.chats[id_] = OllamaChat(ctx.app, id_)
|
|
198
|
+
|
|
199
|
+
# Return the new conversation ID
|
|
200
|
+
return {'id': id_}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@chisel.action(name='stopConversation', types=OLLAMA_CHAT_TYPES)
|
|
204
|
+
def stop_conversation(ctx, req):
|
|
205
|
+
with ctx.app.config() as config:
|
|
206
|
+
id_ = req['id']
|
|
207
|
+
conversation = config_conversation(config, id_)
|
|
208
|
+
if conversation is None:
|
|
209
|
+
raise chisel.ActionError('UnknownConversationID')
|
|
210
|
+
|
|
211
|
+
# Not generating?
|
|
212
|
+
chat = ctx.app.chats.get(id_)
|
|
213
|
+
if chat is None:
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
# Stop the conversation
|
|
217
|
+
chat.stop = True
|
|
218
|
+
del ctx.app.chats[id_]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@chisel.action(name='getConversation', types=OLLAMA_CHAT_TYPES)
|
|
222
|
+
def get_conversation(ctx, req):
|
|
223
|
+
with ctx.app.config() as config:
|
|
224
|
+
id_ = req['id']
|
|
225
|
+
conversation = config_conversation(config, id_)
|
|
226
|
+
if conversation is None:
|
|
227
|
+
raise chisel.ActionError('UnknownConversationID')
|
|
228
|
+
|
|
229
|
+
# Return the conversation
|
|
230
|
+
return {
|
|
231
|
+
'conversation': copy.deepcopy(conversation),
|
|
232
|
+
'generating': id_ in ctx.app.chats
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@chisel.action(name='replyConversation', types=OLLAMA_CHAT_TYPES)
|
|
237
|
+
def reply_conversation(ctx, req):
|
|
238
|
+
with ctx.app.config() as config:
|
|
239
|
+
id_ = req['id']
|
|
240
|
+
conversation = config_conversation(config, id_)
|
|
241
|
+
if conversation is None:
|
|
242
|
+
raise chisel.ActionError('UnknownConversationID')
|
|
243
|
+
|
|
244
|
+
# Busy?
|
|
245
|
+
if id_ in ctx.app.chats:
|
|
246
|
+
raise chisel.ActionError('ConversationBusy')
|
|
247
|
+
|
|
248
|
+
# Add the reply exchange
|
|
249
|
+
conversation['exchanges'].append({
|
|
250
|
+
'user': req['user'],
|
|
251
|
+
'model': ''
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
# Start the model chat
|
|
255
|
+
ctx.app.chats[id_] = OllamaChat(ctx.app, id_)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@chisel.action(name='deleteConversation', types=OLLAMA_CHAT_TYPES)
|
|
259
|
+
def delete_conversation(ctx, req):
|
|
260
|
+
with ctx.app.config(save=True) as config:
|
|
261
|
+
id_ = req['id']
|
|
262
|
+
conversation = config_conversation(config, id_)
|
|
263
|
+
if conversation is None:
|
|
264
|
+
raise chisel.ActionError('UnknownConversationID')
|
|
265
|
+
|
|
266
|
+
# Busy?
|
|
267
|
+
if id_ in ctx.app.chats:
|
|
268
|
+
raise chisel.ActionError('ConversationBusy')
|
|
269
|
+
|
|
270
|
+
# Delete the conversation
|
|
271
|
+
config['conversations'] = [conversation for conversation in config['conversations'] if conversation['id'] != id_]
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@chisel.action(name='deleteConversationExchange', types=OLLAMA_CHAT_TYPES)
|
|
275
|
+
def delete_conversation_exchange(ctx, req):
|
|
276
|
+
with ctx.app.config(save=True) as config:
|
|
277
|
+
id_ = req['id']
|
|
278
|
+
conversation = config_conversation(config, id_)
|
|
279
|
+
if conversation is None:
|
|
280
|
+
raise chisel.ActionError('UnknownConversationID')
|
|
281
|
+
|
|
282
|
+
# Busy?
|
|
283
|
+
if id_ in ctx.app.chats:
|
|
284
|
+
raise chisel.ActionError('ConversationBusy')
|
|
285
|
+
|
|
286
|
+
# Delete the most recent exchange (but not the last one)
|
|
287
|
+
exchanges = conversation['exchanges']
|
|
288
|
+
if len(exchanges) > 1:
|
|
289
|
+
del exchanges[-1]
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@chisel.action(name='regenerateConversationExchange', types=OLLAMA_CHAT_TYPES)
|
|
293
|
+
def regenerate_conversation_exchange(ctx, req):
|
|
294
|
+
with ctx.app.config(save=True) as config:
|
|
295
|
+
id_ = req['id']
|
|
296
|
+
conversation = config_conversation(config, id_)
|
|
297
|
+
if conversation is None:
|
|
298
|
+
raise chisel.ActionError('UnknownConversationID')
|
|
299
|
+
|
|
300
|
+
# Busy?
|
|
301
|
+
if id_ in ctx.app.chats:
|
|
302
|
+
raise chisel.ActionError('ConversationBusy')
|
|
303
|
+
|
|
304
|
+
# Reset the most recent exchange's model response
|
|
305
|
+
exchanges = conversation['exchanges']
|
|
306
|
+
exchanges[-1]['model'] = ''
|
|
307
|
+
|
|
308
|
+
# Start the model chat
|
|
309
|
+
ctx.app.chats[id_] = OllamaChat(ctx.app, id_)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Licensed under the MIT License
|
|
2
|
+
# https://github.com/craigahobbs/ollama-chat/blob/main/LICENSE
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
ollama-chat command-line script main module
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import threading
|
|
10
|
+
import webbrowser
|
|
11
|
+
|
|
12
|
+
import waitress
|
|
13
|
+
|
|
14
|
+
from .app import OllamaChatApplication
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main(argv=None):
|
|
18
|
+
"""
|
|
19
|
+
ollama-chat command-line script main entry point
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Command line arguments
|
|
23
|
+
parser = argparse.ArgumentParser(prog='ollama-chat')
|
|
24
|
+
parser.add_argument('-c', metavar='FILE', dest='config', default='ollama-chat.json',
|
|
25
|
+
help='the configuration file (default is "ollama-chat.json")')
|
|
26
|
+
parser.add_argument('-p', metavar='N', dest='port', type=int, default=8080,
|
|
27
|
+
help='the application port (default is 8080)')
|
|
28
|
+
parser.add_argument('-n', dest='no_browser', action='store_true',
|
|
29
|
+
help="don't open a web browser")
|
|
30
|
+
parser.add_argument('-q', dest='quiet', action='store_true',
|
|
31
|
+
help="don't display access logging")
|
|
32
|
+
args = parser.parse_args(args=argv)
|
|
33
|
+
|
|
34
|
+
# Construct the URL
|
|
35
|
+
host = '127.0.0.1'
|
|
36
|
+
url = f'http://{host}:{args.port}/'
|
|
37
|
+
|
|
38
|
+
# Launch the web browser on a thread so the WSGI application can startup first
|
|
39
|
+
if not args.no_browser:
|
|
40
|
+
webbrowser_thread = threading.Thread(target=webbrowser.open, args=(url,))
|
|
41
|
+
webbrowser_thread.daemon = True
|
|
42
|
+
webbrowser_thread.start()
|
|
43
|
+
|
|
44
|
+
# Create the WSGI application
|
|
45
|
+
wsgiapp = OllamaChatApplication(args.config)
|
|
46
|
+
|
|
47
|
+
# Wrap the WSGI application and the start_response function so we can log status and environ
|
|
48
|
+
def wsgiapp_wrap(environ, start_response):
|
|
49
|
+
def log_start_response(status, response_headers):
|
|
50
|
+
if not args.quiet:
|
|
51
|
+
print(f'ollama-chat: {status[0:3]} {environ["REQUEST_METHOD"]} {environ["PATH_INFO"]} {environ["QUERY_STRING"]}')
|
|
52
|
+
return start_response(status, response_headers)
|
|
53
|
+
return wsgiapp(environ, log_start_response)
|
|
54
|
+
|
|
55
|
+
# Host the application
|
|
56
|
+
if not args.quiet:
|
|
57
|
+
print(f'ollama-chat: Serving at {url} ...')
|
|
58
|
+
waitress.serve(wsgiapp_wrap, port=args.port)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Licensed under the MIT License
|
|
2
|
+
# https://github.com/craigahobbs/ollama-chat/blob/main/LICENSE
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
The ollama chat manager
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import threading
|
|
9
|
+
|
|
10
|
+
import ollama
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OllamaChat():
|
|
14
|
+
"""
|
|
15
|
+
The ollama chat manager class
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
__slots__ = ('app', 'conversation_id', 'stop')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def __init__(self, app, conversation_id):
|
|
22
|
+
self.app = app
|
|
23
|
+
self.conversation_id = conversation_id
|
|
24
|
+
self.stop = False
|
|
25
|
+
|
|
26
|
+
# Start the chat thread
|
|
27
|
+
chat_thread = threading.Thread(target=OllamaChat.chat_thread_fn, args=(self,))
|
|
28
|
+
chat_thread.daemon = True
|
|
29
|
+
chat_thread.start()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def chat_thread_fn(chat):
|
|
34
|
+
try:
|
|
35
|
+
# Create the Ollama messages from the conversation
|
|
36
|
+
messages = []
|
|
37
|
+
with chat.app.config() as config:
|
|
38
|
+
conversation = config_conversation(config, chat.conversation_id)
|
|
39
|
+
model = conversation['model']
|
|
40
|
+
for exchange in conversation['exchanges']:
|
|
41
|
+
messages.append({'role': 'user', 'content': exchange['user']})
|
|
42
|
+
if exchange['model'] != '':
|
|
43
|
+
messages.append({'role': 'assistant', 'content': exchange['model']})
|
|
44
|
+
|
|
45
|
+
# Start the chat
|
|
46
|
+
stream = ollama.chat(model=model, messages=messages, stream=True)
|
|
47
|
+
|
|
48
|
+
# Stream the chat response
|
|
49
|
+
for chunk in stream:
|
|
50
|
+
# If stopped, return immediately. The chat is deleted by the stopper.
|
|
51
|
+
if chat.stop:
|
|
52
|
+
stream.close()
|
|
53
|
+
break
|
|
54
|
+
|
|
55
|
+
# Update the conversation
|
|
56
|
+
with chat.app.config() as config:
|
|
57
|
+
conversation = config_conversation(config, chat.conversation_id)
|
|
58
|
+
exchange = conversation['exchanges'][-1]
|
|
59
|
+
exchange['model'] += chunk['message']['content']
|
|
60
|
+
|
|
61
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
62
|
+
# Communicate the error
|
|
63
|
+
with chat.app.config() as config:
|
|
64
|
+
conversation = config_conversation(config, chat.conversation_id)
|
|
65
|
+
exchange = conversation['exchanges'][-1]
|
|
66
|
+
exchange['model'] += f'\n**ERROR:** {exc}'
|
|
67
|
+
|
|
68
|
+
# Save the conversation
|
|
69
|
+
with chat.app.config(save=True):
|
|
70
|
+
# Delete the application's chat entry
|
|
71
|
+
if chat.conversation_id in chat.app.chats:
|
|
72
|
+
del chat.app.chats[chat.conversation_id]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def config_conversation(config, id_):
|
|
76
|
+
"""
|
|
77
|
+
Helper to find a conversation by ID
|
|
78
|
+
"""
|
|
79
|
+
return next((conv for conv in config['conversations'] if conv['id'] == id_), None)
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Licensed under the MIT License
|
|
2
|
+
# https://github.com/craigahobbs/ollama-chat/blob/main/LICENSE
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
group "Ollama Chat JSON"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# The Ollama Chat config file format
|
|
9
|
+
struct OllamaChatConfig
|
|
10
|
+
|
|
11
|
+
# The current model name
|
|
12
|
+
optional string model
|
|
13
|
+
|
|
14
|
+
# The saved conversations
|
|
15
|
+
Conversation[] conversations
|
|
16
|
+
|
|
17
|
+
# If true, don't save the config file
|
|
18
|
+
optional bool noSave
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# The user-model conversation info
|
|
22
|
+
struct ConversationInfo
|
|
23
|
+
|
|
24
|
+
# The conversation identifier
|
|
25
|
+
string id
|
|
26
|
+
|
|
27
|
+
# The model name
|
|
28
|
+
string model
|
|
29
|
+
|
|
30
|
+
# The conversation title
|
|
31
|
+
string title
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# A user-model conversation
|
|
35
|
+
struct Conversation (ConversationInfo)
|
|
36
|
+
|
|
37
|
+
# The conversation's exchanges
|
|
38
|
+
ConversationExchange[len > 0] exchanges
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# A conversation user-model exchange
|
|
42
|
+
struct ConversationExchange
|
|
43
|
+
|
|
44
|
+
# The user prompt
|
|
45
|
+
string(len > 0) user
|
|
46
|
+
|
|
47
|
+
# The model response
|
|
48
|
+
string model
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
group "Ollama Chat API"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Get the current model name
|
|
55
|
+
action getModels
|
|
56
|
+
urls
|
|
57
|
+
GET
|
|
58
|
+
|
|
59
|
+
output
|
|
60
|
+
# The local models
|
|
61
|
+
ModelInfo[] models
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Model information struct
|
|
65
|
+
struct ModelInfo
|
|
66
|
+
|
|
67
|
+
# The current model name
|
|
68
|
+
string model
|
|
69
|
+
|
|
70
|
+
# The model size, in bytes
|
|
71
|
+
int size
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Get the current model name
|
|
75
|
+
action getModel
|
|
76
|
+
urls
|
|
77
|
+
GET
|
|
78
|
+
|
|
79
|
+
output
|
|
80
|
+
# The current model name
|
|
81
|
+
string model
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Set the current model name
|
|
85
|
+
action setModel
|
|
86
|
+
urls
|
|
87
|
+
POST
|
|
88
|
+
|
|
89
|
+
input
|
|
90
|
+
# The model name
|
|
91
|
+
string model
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Get the list of conversations
|
|
95
|
+
action getConversations
|
|
96
|
+
urls
|
|
97
|
+
GET
|
|
98
|
+
|
|
99
|
+
output
|
|
100
|
+
# The conversations
|
|
101
|
+
ConversationInfoEx[] conversations
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Conversation information struct with generating state
|
|
105
|
+
struct ConversationInfoEx (ConversationInfo)
|
|
106
|
+
|
|
107
|
+
# If True, the latest exchange is actively generating
|
|
108
|
+
bool generating
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Start a conversation
|
|
112
|
+
action startConversation
|
|
113
|
+
urls
|
|
114
|
+
POST
|
|
115
|
+
|
|
116
|
+
input
|
|
117
|
+
# The user prompt
|
|
118
|
+
string(len > 0) user
|
|
119
|
+
|
|
120
|
+
output
|
|
121
|
+
# The new conversation ID
|
|
122
|
+
string id
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# Stop a generating conversation
|
|
126
|
+
action stopConversation
|
|
127
|
+
urls
|
|
128
|
+
POST
|
|
129
|
+
|
|
130
|
+
input
|
|
131
|
+
# The conversation identifier
|
|
132
|
+
string id
|
|
133
|
+
|
|
134
|
+
errors
|
|
135
|
+
UnknownConversationID
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# Get a conversation
|
|
139
|
+
action getConversation
|
|
140
|
+
urls
|
|
141
|
+
GET
|
|
142
|
+
|
|
143
|
+
query
|
|
144
|
+
# The conversation identifier
|
|
145
|
+
string id
|
|
146
|
+
|
|
147
|
+
output
|
|
148
|
+
# The conversation
|
|
149
|
+
Conversation conversation
|
|
150
|
+
|
|
151
|
+
# If True, the latest exchange is actively generating
|
|
152
|
+
bool generating
|
|
153
|
+
|
|
154
|
+
errors
|
|
155
|
+
UnknownConversationID
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# Reply to a conversation
|
|
159
|
+
action replyConversation
|
|
160
|
+
urls
|
|
161
|
+
POST
|
|
162
|
+
|
|
163
|
+
input
|
|
164
|
+
# The conversation identifier
|
|
165
|
+
string id
|
|
166
|
+
|
|
167
|
+
# The user reply prompt
|
|
168
|
+
string(len > 0) user
|
|
169
|
+
|
|
170
|
+
errors
|
|
171
|
+
UnknownConversationID
|
|
172
|
+
ConversationBusy
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# Delete a conversation
|
|
176
|
+
action deleteConversation
|
|
177
|
+
urls
|
|
178
|
+
POST
|
|
179
|
+
|
|
180
|
+
input
|
|
181
|
+
# The conversation identifier
|
|
182
|
+
string id
|
|
183
|
+
|
|
184
|
+
errors
|
|
185
|
+
UnknownConversationID
|
|
186
|
+
ConversationBusy
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# Delete the most recent exchange of a conversation
|
|
190
|
+
action deleteConversationExchange
|
|
191
|
+
urls
|
|
192
|
+
POST
|
|
193
|
+
|
|
194
|
+
input
|
|
195
|
+
# The conversation identifier
|
|
196
|
+
string id
|
|
197
|
+
|
|
198
|
+
errors
|
|
199
|
+
UnknownConversationID
|
|
200
|
+
ConversationBusy
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# Regnerate the model's response of the most recent exchange of a conversation
|
|
204
|
+
action regenerateConversationExchange
|
|
205
|
+
urls
|
|
206
|
+
POST
|
|
207
|
+
|
|
208
|
+
input
|
|
209
|
+
# The conversation identifier
|
|
210
|
+
string id
|
|
211
|
+
|
|
212
|
+
errors
|
|
213
|
+
UnknownConversationID
|
|
214
|
+
ConversationBusy
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ollama-chat
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: ollama-chat
|
|
5
|
+
Home-page: https://github.com/craigahobbs/ollama-chat
|
|
6
|
+
Author: Craig A. Hobbs
|
|
7
|
+
Author-email: craigahobbs@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: ollama-chat
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: chisel>=1.2.7
|
|
23
|
+
Requires-Dist: ollama>=0.1.9
|
|
24
|
+
Requires-Dist: waitress>=3.0.0
|
|
25
|
+
|
|
26
|
+
# ollama-chat
|
|
27
|
+
|
|
28
|
+
[](https://pypi.org/project/ollama-chat/)
|
|
29
|
+
[](https://pypi.org/project/ollama-chat/)
|
|
30
|
+
[](https://github.com/craigahobbs/ollama-chat/blob/main/LICENSE)
|
|
31
|
+
[](https://pypi.org/project/ollama-chat/)
|
|
32
|
+
|
|
33
|
+
**Ollama Chat** is a simple yet useful web chat client for
|
|
34
|
+
[Ollama](https://ollama.com)
|
|
35
|
+
that allows you to chat locally (and privately) with
|
|
36
|
+
[open-source LLMs](https://ollama.com/library).
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Installation
|
|
40
|
+
|
|
41
|
+
To get up and running with Ollama Chat follows these steps:
|
|
42
|
+
|
|
43
|
+
1. Install and start [Ollama](https://ollama.com)
|
|
44
|
+
|
|
45
|
+
2. Install Ollama Chat
|
|
46
|
+
|
|
47
|
+
~~~
|
|
48
|
+
pip install ollama-chat
|
|
49
|
+
~~~
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Starting Ollama Chat
|
|
53
|
+
|
|
54
|
+
To start Ollama Chat, open a terminal prompt and run the Ollama Chat application:
|
|
55
|
+
|
|
56
|
+
~~~
|
|
57
|
+
ollama-chat
|
|
58
|
+
~~~
|
|
59
|
+
|
|
60
|
+
A web browser is launched and opens the Ollama Chat web application.
|
|
61
|
+
|
|
62
|
+
By default, a configuration file, "ollama-chat.json", is created in the current directory to save
|
|
63
|
+
your conversations.
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
## Future Features
|
|
67
|
+
|
|
68
|
+
In no particular order...
|
|
69
|
+
|
|
70
|
+
- Save conversation as Markdown file
|
|
71
|
+
|
|
72
|
+
- Conversation title edit
|
|
73
|
+
|
|
74
|
+
- File / Directory / URL text inclusion in prompt
|
|
75
|
+
|
|
76
|
+
- Local model management (pull, rm)
|
|
77
|
+
- [Models JSON](https://huggingface.co/api/models)
|
|
78
|
+
|
|
79
|
+
- Prompt library
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
This package is developed using [python-build](https://github.com/craigahobbs/python-build#readme).
|
|
85
|
+
It was started using [python-template](https://github.com/craigahobbs/python-template#readme) as follows:
|
|
86
|
+
|
|
87
|
+
~~~
|
|
88
|
+
template-specialize python-template/template/ ollama-chat/ -k package ollama-chat -k name 'Craig A. Hobbs' -k email 'craigahobbs@gmail.com' -k github 'craigahobbs' -k noapi 1
|
|
89
|
+
~~~
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.cfg
|
|
5
|
+
src/ollama_chat/__init__.py
|
|
6
|
+
src/ollama_chat/__main__.py
|
|
7
|
+
src/ollama_chat/app.py
|
|
8
|
+
src/ollama_chat/main.py
|
|
9
|
+
src/ollama_chat/ollama.py
|
|
10
|
+
src/ollama_chat.egg-info/PKG-INFO
|
|
11
|
+
src/ollama_chat.egg-info/SOURCES.txt
|
|
12
|
+
src/ollama_chat.egg-info/dependency_links.txt
|
|
13
|
+
src/ollama_chat.egg-info/entry_points.txt
|
|
14
|
+
src/ollama_chat.egg-info/requires.txt
|
|
15
|
+
src/ollama_chat.egg-info/top_level.txt
|
|
16
|
+
src/ollama_chat/static/ollamaChat.smd
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ollama_chat
|