mem-llm 1.0.2__py3-none-any.whl → 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mem-llm might be problematic. Click here for more details.
- mem_llm/__init__.py +71 -8
- mem_llm/api_server.py +595 -0
- mem_llm/base_llm_client.py +201 -0
- mem_llm/builtin_tools.py +311 -0
- mem_llm/builtin_tools_async.py +170 -0
- mem_llm/cli.py +254 -0
- mem_llm/clients/__init__.py +22 -0
- mem_llm/clients/lmstudio_client.py +393 -0
- mem_llm/clients/ollama_client.py +354 -0
- mem_llm/config.yaml.example +1 -1
- mem_llm/config_from_docs.py +1 -1
- mem_llm/config_manager.py +5 -3
- mem_llm/conversation_summarizer.py +372 -0
- mem_llm/data_export_import.py +640 -0
- mem_llm/dynamic_prompt.py +298 -0
- mem_llm/llm_client.py +77 -14
- mem_llm/llm_client_factory.py +260 -0
- mem_llm/logger.py +129 -0
- mem_llm/mem_agent.py +1178 -87
- mem_llm/memory_db.py +290 -59
- mem_llm/memory_manager.py +60 -1
- mem_llm/prompt_security.py +304 -0
- mem_llm/response_metrics.py +221 -0
- mem_llm/retry_handler.py +193 -0
- mem_llm/thread_safe_db.py +301 -0
- mem_llm/tool_system.py +537 -0
- mem_llm/vector_store.py +278 -0
- mem_llm/web_launcher.py +129 -0
- mem_llm/web_ui/README.md +44 -0
- mem_llm/web_ui/__init__.py +7 -0
- mem_llm/web_ui/index.html +641 -0
- mem_llm/web_ui/memory.html +569 -0
- mem_llm/web_ui/metrics.html +75 -0
- mem_llm-2.1.0.dist-info/METADATA +753 -0
- mem_llm-2.1.0.dist-info/RECORD +40 -0
- {mem_llm-1.0.2.dist-info → mem_llm-2.1.0.dist-info}/WHEEL +1 -1
- mem_llm-2.1.0.dist-info/entry_points.txt +3 -0
- mem_llm/prompt_templates.py +0 -244
- mem_llm-1.0.2.dist-info/METADATA +0 -382
- mem_llm-1.0.2.dist-info/RECORD +0 -15
- {mem_llm-1.0.2.dist-info → mem_llm-2.1.0.dist-info}/top_level.txt +0 -0
mem_llm/vector_store.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Vector Store Abstraction Layer
|
|
3
|
+
Supports multiple vector databases (Chroma, FAISS, etc.)
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import List, Dict, Optional, Any
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VectorStore(ABC):
|
|
14
|
+
"""Abstract interface for vector stores"""
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def add_documents(self, documents: List[Dict[str, Any]]) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Add documents to vector store
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
documents: List of dicts with 'id', 'text', 'metadata'
|
|
23
|
+
"""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def search(self, query: str, limit: int = 5, filter_metadata: Optional[Dict] = None) -> List[Dict[str, Any]]:
|
|
28
|
+
"""
|
|
29
|
+
Search similar documents
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
query: Search query text
|
|
33
|
+
limit: Maximum number of results
|
|
34
|
+
filter_metadata: Optional metadata filters
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List of similar documents with scores
|
|
38
|
+
"""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def delete_collection(self) -> None:
|
|
43
|
+
"""Delete all vectors in collection"""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
48
|
+
"""Get statistics about the vector store"""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
import chromadb
|
|
54
|
+
from chromadb.config import Settings
|
|
55
|
+
CHROMA_AVAILABLE = True
|
|
56
|
+
except ImportError:
|
|
57
|
+
CHROMA_AVAILABLE = False
|
|
58
|
+
# Don't warn on import, only when actually trying to use it
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ChromaVectorStore(VectorStore):
|
|
62
|
+
"""ChromaDB implementation of VectorStore"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, collection_name: str = "knowledge_base",
|
|
65
|
+
persist_directory: Optional[str] = None,
|
|
66
|
+
embedding_model: str = "all-MiniLM-L6-v2"):
|
|
67
|
+
"""
|
|
68
|
+
Initialize ChromaDB vector store
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
collection_name: Name of the collection
|
|
72
|
+
persist_directory: Directory to persist data (None = in-memory)
|
|
73
|
+
embedding_model: Embedding model name (sentence-transformers compatible)
|
|
74
|
+
"""
|
|
75
|
+
if not CHROMA_AVAILABLE:
|
|
76
|
+
raise ImportError(
|
|
77
|
+
"ChromaDB is not installed. Install with: pip install chromadb"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
self.collection_name = collection_name
|
|
81
|
+
self.persist_directory = persist_directory
|
|
82
|
+
self.embedding_model = embedding_model
|
|
83
|
+
|
|
84
|
+
# Initialize Chroma client
|
|
85
|
+
if persist_directory:
|
|
86
|
+
self.client = chromadb.PersistentClient(path=persist_directory)
|
|
87
|
+
else:
|
|
88
|
+
self.client = chromadb.Client()
|
|
89
|
+
|
|
90
|
+
# Lazy load embedding model
|
|
91
|
+
self._embedding_fn = None
|
|
92
|
+
|
|
93
|
+
# Get or create collection with embedding function
|
|
94
|
+
try:
|
|
95
|
+
# Create embedding function
|
|
96
|
+
embedding_fn = self._get_embedding_function()
|
|
97
|
+
|
|
98
|
+
self.collection = self.client.get_or_create_collection(
|
|
99
|
+
name=collection_name,
|
|
100
|
+
embedding_function=embedding_fn,
|
|
101
|
+
metadata={"hnsw:space": "cosine"}
|
|
102
|
+
)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.error(f"Failed to create Chroma collection: {e}")
|
|
105
|
+
raise
|
|
106
|
+
|
|
107
|
+
def _get_embedding_function(self):
|
|
108
|
+
"""Lazy load embedding function"""
|
|
109
|
+
if self._embedding_fn is None:
|
|
110
|
+
try:
|
|
111
|
+
# Try to use ChromaDB's native SentenceTransformerEmbeddingFunction
|
|
112
|
+
try:
|
|
113
|
+
# Try different import paths for ChromaDB embedding functions
|
|
114
|
+
try:
|
|
115
|
+
from chromadb.utils import embedding_functions
|
|
116
|
+
embedding_fn_class = embedding_functions.SentenceTransformerEmbeddingFunction
|
|
117
|
+
except (ImportError, AttributeError):
|
|
118
|
+
try:
|
|
119
|
+
from chromadb.utils.embedding_functions import SentenceTransformerEmbeddingFunction as embedding_fn_class
|
|
120
|
+
except ImportError:
|
|
121
|
+
embedding_fn_class = None
|
|
122
|
+
|
|
123
|
+
if embedding_fn_class:
|
|
124
|
+
self._embedding_fn = embedding_fn_class(model_name=self.embedding_model)
|
|
125
|
+
logger.info(f"Loaded embedding model using ChromaDB native function: {self.embedding_model}")
|
|
126
|
+
else:
|
|
127
|
+
raise AttributeError("SentenceTransformerEmbeddingFunction not found")
|
|
128
|
+
|
|
129
|
+
except (ImportError, AttributeError, Exception) as e:
|
|
130
|
+
# Fallback: Custom embedding function wrapper compatible with ChromaDB
|
|
131
|
+
from sentence_transformers import SentenceTransformer
|
|
132
|
+
model = SentenceTransformer(self.embedding_model)
|
|
133
|
+
|
|
134
|
+
class CustomEmbeddingFunction:
|
|
135
|
+
def __init__(self, model, model_name):
|
|
136
|
+
self.model = model
|
|
137
|
+
self.model_name = model_name
|
|
138
|
+
self.name = model_name # ChromaDB may check for 'name' attribute
|
|
139
|
+
|
|
140
|
+
def __call__(self, texts: List[str]) -> List[List[float]]:
|
|
141
|
+
embeddings = self.model.encode(texts, show_progress_bar=False)
|
|
142
|
+
return embeddings.tolist()
|
|
143
|
+
|
|
144
|
+
def encode_queries(self, queries: List[str]) -> List[List[float]]:
|
|
145
|
+
return self.__call__(queries)
|
|
146
|
+
|
|
147
|
+
self._embedding_fn = CustomEmbeddingFunction(model, self.embedding_model)
|
|
148
|
+
logger.info(f"Loaded embedding model using custom wrapper: {self.embedding_model} (fallback: {e})")
|
|
149
|
+
except ImportError:
|
|
150
|
+
raise ImportError(
|
|
151
|
+
"sentence-transformers not installed. "
|
|
152
|
+
"Install with: pip install sentence-transformers"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return self._embedding_fn
|
|
156
|
+
|
|
157
|
+
def add_documents(self, documents: List[Dict[str, Any]]) -> None:
|
|
158
|
+
"""Add documents to ChromaDB"""
|
|
159
|
+
if not documents:
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
# Prepare data
|
|
163
|
+
ids = []
|
|
164
|
+
texts = []
|
|
165
|
+
metadatas = []
|
|
166
|
+
|
|
167
|
+
for doc in documents:
|
|
168
|
+
doc_id = str(doc.get('id', doc.get('text', ''))[:100])
|
|
169
|
+
# Ensure unique IDs
|
|
170
|
+
if doc_id in ids:
|
|
171
|
+
doc_id = f"{doc_id}_{len(ids)}"
|
|
172
|
+
ids.append(doc_id)
|
|
173
|
+
texts.append(doc['text'])
|
|
174
|
+
# Ensure metadata values are JSON-serializable
|
|
175
|
+
metadata = doc.get('metadata', {})
|
|
176
|
+
clean_metadata = {}
|
|
177
|
+
for k, v in metadata.items():
|
|
178
|
+
if isinstance(v, (str, int, float, bool)) or v is None:
|
|
179
|
+
clean_metadata[k] = v
|
|
180
|
+
else:
|
|
181
|
+
clean_metadata[k] = str(v)
|
|
182
|
+
metadatas.append(clean_metadata)
|
|
183
|
+
|
|
184
|
+
# Add to collection (Chroma will use embedding function automatically)
|
|
185
|
+
try:
|
|
186
|
+
self.collection.add(
|
|
187
|
+
ids=ids,
|
|
188
|
+
documents=texts,
|
|
189
|
+
metadatas=metadatas
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
logger.debug(f"Added {len(documents)} documents to Chroma")
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.error(f"Error adding documents to Chroma: {e}")
|
|
195
|
+
raise
|
|
196
|
+
|
|
197
|
+
def search(self, query: str, limit: int = 5,
|
|
198
|
+
filter_metadata: Optional[Dict] = None) -> List[Dict[str, Any]]:
|
|
199
|
+
"""Search in ChromaDB"""
|
|
200
|
+
try:
|
|
201
|
+
# Build where clause for metadata filtering
|
|
202
|
+
where = None
|
|
203
|
+
if filter_metadata:
|
|
204
|
+
where = filter_metadata
|
|
205
|
+
|
|
206
|
+
# Search (Chroma will use embedding function automatically)
|
|
207
|
+
results = self.collection.query(
|
|
208
|
+
query_texts=[query],
|
|
209
|
+
n_results=limit,
|
|
210
|
+
where=where
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Format results
|
|
214
|
+
formatted_results = []
|
|
215
|
+
if results.get('documents') and len(results['documents']) > 0 and len(results['documents'][0]) > 0:
|
|
216
|
+
num_results = len(results['documents'][0])
|
|
217
|
+
distances = results.get('distances', [[0.0] * num_results])
|
|
218
|
+
|
|
219
|
+
for i in range(num_results):
|
|
220
|
+
# ChromaDB uses cosine distance (0 = identical, 1 = opposite)
|
|
221
|
+
# Convert to similarity score (1 = identical, 0 = opposite)
|
|
222
|
+
distance = distances[0][i] if distances and len(distances[0]) > i else 0.0
|
|
223
|
+
similarity = 1.0 - distance if distance <= 1.0 else max(0.0, 1.0 / (1.0 + distance))
|
|
224
|
+
|
|
225
|
+
formatted_results.append({
|
|
226
|
+
'id': results['ids'][0][i] if results.get('ids') and len(results['ids'][0]) > i else f"doc_{i}",
|
|
227
|
+
'text': results['documents'][0][i],
|
|
228
|
+
'metadata': results['metadatas'][0][i] if results.get('metadatas') and len(results['metadatas'][0]) > i else {},
|
|
229
|
+
'score': similarity
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
return formatted_results
|
|
233
|
+
except Exception as e:
|
|
234
|
+
logger.error(f"Error searching Chroma: {e}")
|
|
235
|
+
return []
|
|
236
|
+
|
|
237
|
+
def delete_collection(self) -> None:
|
|
238
|
+
"""Delete collection"""
|
|
239
|
+
try:
|
|
240
|
+
self.client.delete_collection(self.collection_name)
|
|
241
|
+
logger.info(f"Deleted Chroma collection: {self.collection_name}")
|
|
242
|
+
except Exception as e:
|
|
243
|
+
logger.error(f"Error deleting collection: {e}")
|
|
244
|
+
|
|
245
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
246
|
+
"""Get collection statistics"""
|
|
247
|
+
try:
|
|
248
|
+
count = self.collection.count()
|
|
249
|
+
return {
|
|
250
|
+
'total_documents': count,
|
|
251
|
+
'collection_name': self.collection_name,
|
|
252
|
+
'embedding_model': self.embedding_model
|
|
253
|
+
}
|
|
254
|
+
except Exception as e:
|
|
255
|
+
logger.error(f"Error getting stats: {e}")
|
|
256
|
+
return {'total_documents': 0}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def create_vector_store(store_type: str = "chroma", **kwargs) -> Optional[VectorStore]:
|
|
260
|
+
"""
|
|
261
|
+
Factory function to create vector store
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
store_type: Type of vector store ('chroma', 'faiss', etc.)
|
|
265
|
+
**kwargs: Store-specific parameters
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
VectorStore instance or None if not available
|
|
269
|
+
"""
|
|
270
|
+
if store_type == "chroma":
|
|
271
|
+
if not CHROMA_AVAILABLE:
|
|
272
|
+
logger.info("ℹ️ Vector search disabled (ChromaDB not installed). For semantic search, run: pip install chromadb sentence-transformers")
|
|
273
|
+
return None
|
|
274
|
+
return ChromaVectorStore(**kwargs)
|
|
275
|
+
else:
|
|
276
|
+
logger.warning(f"Unknown vector store type: {store_type}")
|
|
277
|
+
return None
|
|
278
|
+
|
mem_llm/web_launcher.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Web UI Launcher for Mem-LLM
|
|
4
|
+
Starts the API server and opens the Web UI in the browser.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
import webbrowser
|
|
10
|
+
import requests
|
|
11
|
+
import threading
|
|
12
|
+
|
|
13
|
+
def check_backend_available():
|
|
14
|
+
"""Check if Ollama or LM Studio is available."""
|
|
15
|
+
backends = []
|
|
16
|
+
|
|
17
|
+
# Check Ollama
|
|
18
|
+
try:
|
|
19
|
+
response = requests.get("http://localhost:11434/api/tags", timeout=2)
|
|
20
|
+
if response.status_code == 200:
|
|
21
|
+
backends.append("Ollama (http://localhost:11434)")
|
|
22
|
+
except:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
# Check LM Studio
|
|
26
|
+
try:
|
|
27
|
+
response = requests.get("http://localhost:1234/v1/models", timeout=2)
|
|
28
|
+
if response.status_code == 200:
|
|
29
|
+
backends.append("LM Studio (http://localhost:1234)")
|
|
30
|
+
except:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
return backends
|
|
34
|
+
|
|
35
|
+
def check_api_ready(url="http://localhost:8000/api/v1/health", timeout=30):
|
|
36
|
+
"""Wait for API server to be ready."""
|
|
37
|
+
start_time = time.time()
|
|
38
|
+
while time.time() - start_time < timeout:
|
|
39
|
+
try:
|
|
40
|
+
response = requests.get(url, timeout=1)
|
|
41
|
+
if response.status_code == 200:
|
|
42
|
+
return True
|
|
43
|
+
except:
|
|
44
|
+
pass
|
|
45
|
+
time.sleep(0.5)
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
def start_api_server():
|
|
49
|
+
"""Start the FastAPI server in a subprocess."""
|
|
50
|
+
try:
|
|
51
|
+
# Import here to avoid circular imports
|
|
52
|
+
from mem_llm.api_server import app
|
|
53
|
+
import uvicorn
|
|
54
|
+
|
|
55
|
+
# Run server in a thread
|
|
56
|
+
def run_server():
|
|
57
|
+
uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")
|
|
58
|
+
|
|
59
|
+
server_thread = threading.Thread(target=run_server, daemon=True)
|
|
60
|
+
server_thread.start()
|
|
61
|
+
|
|
62
|
+
return server_thread
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print(f"❌ Failed to start API server: {e}")
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
def main():
|
|
68
|
+
"""Main entry point for the web launcher."""
|
|
69
|
+
print("🚀 Mem-LLM Web UI Launcher")
|
|
70
|
+
print("=" * 50)
|
|
71
|
+
|
|
72
|
+
# Check available backends
|
|
73
|
+
print("\n🔍 Checking available backends...")
|
|
74
|
+
backends = check_backend_available()
|
|
75
|
+
|
|
76
|
+
if backends:
|
|
77
|
+
print("✅ Available backends:")
|
|
78
|
+
for backend in backends:
|
|
79
|
+
print(f" - {backend}")
|
|
80
|
+
else:
|
|
81
|
+
print("⚠️ No local backends detected!")
|
|
82
|
+
print(" Please start Ollama or LM Studio first:")
|
|
83
|
+
print(" - Ollama: https://ollama.ai")
|
|
84
|
+
print(" - LM Studio: https://lmstudio.ai")
|
|
85
|
+
response = input("\n Continue anyway? (y/n): ")
|
|
86
|
+
if response.lower() != 'y':
|
|
87
|
+
sys.exit(0)
|
|
88
|
+
|
|
89
|
+
# Start API server
|
|
90
|
+
print("\n🌐 Starting API server...")
|
|
91
|
+
server_thread = start_api_server()
|
|
92
|
+
|
|
93
|
+
if not server_thread:
|
|
94
|
+
print("❌ Failed to start API server!")
|
|
95
|
+
sys.exit(1)
|
|
96
|
+
|
|
97
|
+
# Wait for server to be ready
|
|
98
|
+
print("⏳ Waiting for API server to be ready...")
|
|
99
|
+
if not check_api_ready():
|
|
100
|
+
print("❌ API server failed to start within 30 seconds!")
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
|
|
103
|
+
print("✅ API server is ready!")
|
|
104
|
+
|
|
105
|
+
# Open browser
|
|
106
|
+
web_url = "http://localhost:8000/"
|
|
107
|
+
print(f"\n🌐 Opening Web UI at {web_url}")
|
|
108
|
+
webbrowser.open(web_url)
|
|
109
|
+
|
|
110
|
+
print("\n" + "=" * 50)
|
|
111
|
+
print("✅ Mem-LLM Web UI is running!")
|
|
112
|
+
print(" - Chat: http://localhost:8000/")
|
|
113
|
+
print(" - Memory: http://localhost:8000/memory")
|
|
114
|
+
print(" - Metrics: http://localhost:8000/metrics")
|
|
115
|
+
print(" - API Docs: http://localhost:8000/docs")
|
|
116
|
+
print("\n Press Ctrl+C to stop the server.")
|
|
117
|
+
print("=" * 50)
|
|
118
|
+
|
|
119
|
+
# Keep the main thread alive
|
|
120
|
+
try:
|
|
121
|
+
while True:
|
|
122
|
+
time.sleep(1)
|
|
123
|
+
except KeyboardInterrupt:
|
|
124
|
+
print("\n\n👋 Shutting down...")
|
|
125
|
+
sys.exit(0)
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
main()
|
|
129
|
+
|
mem_llm/web_ui/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Mem-LLM Web UI
|
|
2
|
+
|
|
3
|
+
Modern web interface for Mem-LLM with streaming support, memory management, and metrics dashboard.
|
|
4
|
+
|
|
5
|
+
## 📄 Pages
|
|
6
|
+
|
|
7
|
+
1. **💬 Chat (index.html)** - Main chat interface with real-time streaming
|
|
8
|
+
2. **🧠 Memory (memory.html)** - Memory management and search
|
|
9
|
+
3. **📊 Metrics (metrics.html)** - System metrics and statistics
|
|
10
|
+
|
|
11
|
+
## 🚀 Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Install mem-llm with API support
|
|
15
|
+
pip install mem-llm[api]
|
|
16
|
+
|
|
17
|
+
# Launch Web UI (recommended)
|
|
18
|
+
mem-llm-web
|
|
19
|
+
|
|
20
|
+
# Or use launcher script
|
|
21
|
+
python start_web_ui.py
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 📋 Requirements
|
|
25
|
+
|
|
26
|
+
- Python 3.8+
|
|
27
|
+
- FastAPI
|
|
28
|
+
- Uvicorn
|
|
29
|
+
- WebSockets
|
|
30
|
+
|
|
31
|
+
## 🔧 Configuration
|
|
32
|
+
|
|
33
|
+
Configure backend and model in the Web UI sidebar, or edit `api_server.py` defaults.
|
|
34
|
+
|
|
35
|
+
## 📚 More Info
|
|
36
|
+
|
|
37
|
+
- [Main README](../README.md)
|
|
38
|
+
- [API Docs](http://localhost:8000/docs)
|
|
39
|
+
- [Examples](../../examples/)
|
|
40
|
+
|
|
41
|
+
## 📄 License
|
|
42
|
+
|
|
43
|
+
MIT License
|
|
44
|
+
|