tommytracx commited on
Commit
b82828c
·
verified ·
1 Parent(s): 28c2439

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +207 -386
app.py CHANGED
@@ -1,5 +1,5 @@
1
  # app.py
2
- from flask import Flask, request, jsonify, render_template_string
3
  import os
4
  import requests
5
  import json
@@ -11,7 +11,7 @@ app = Flask(__name__)
11
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
12
 
13
  # Configuration
14
- OLLAMA_BASE_URL = os.getenv('OLLAMA_BASE_URL', 'http://localhost:11434')
15
  ALLOWED_MODELS = os.getenv('ALLOWED_MODELS', 'llama2,llama2:13b,llama2:70b,codellama,neural-chat,gemma-3-270m').split(',')
16
  MAX_TOKENS = int(os.getenv('MAX_TOKENS', '2048'))
17
  TEMPERATURE = float(os.getenv('TEMPERATURE', '0.7'))
@@ -43,8 +43,21 @@ class OllamaManager:
43
  """Return the list of available models."""
44
  return self.available_models
45
 
46
- def generate(self, model_name: str, prompt: str, **kwargs) -> Dict[str, Any]:
47
- """Generate text using a model."""
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  if model_name not in self.available_models:
49
  return {"status": "error", "message": f"Model {model_name} not available"}
50
 
@@ -52,44 +65,53 @@ class OllamaManager:
52
  payload = {
53
  "model": model_name,
54
  "prompt": prompt,
55
- "stream": False,
56
  **kwargs
57
  }
58
- response = requests.post(f"{self.base_url}/api/generate", json=payload, timeout=120)
59
- response.raise_for_status()
60
- data = response.json()
61
- return {
62
- "status": "success",
63
- "response": data.get('response', ''),
64
- "model": model_name,
65
- "usage": data.get('usage', {})
66
- }
 
 
 
 
 
67
  except Exception as e:
68
  logging.error(f"Error generating response: {e}")
69
  return {"status": "error", "message": str(e)}
70
-
71
- def health_check(self) -> Dict[str, Any]:
72
- """Check the health of the Ollama API."""
73
- try:
74
- response = requests.get(f"{self.base_url}/api/tags", timeout=10)
75
- response.raise_for_status()
76
- return {"status": "healthy", "available_models": len(self.available_models)}
77
- except Exception as e:
78
- logging.error(f"Health check failed: {e}")
79
- return {"status": "unhealthy", "error": str(e)}
80
 
81
  # Initialize Ollama manager
82
  ollama_manager = OllamaManager(OLLAMA_BASE_URL)
83
 
84
- # HTML template for the chat interface (unchanged from original)
85
  HTML_TEMPLATE = '''
86
  <!DOCTYPE html>
87
  <html lang="en">
88
  <head>
89
  <meta charset="UTF-8">
90
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
91
- <title>OpenWebUI - Ollama Chat</title>
92
  <style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  * {
94
  margin: 0;
95
  padding: 0;
@@ -97,385 +119,156 @@ HTML_TEMPLATE = '''
97
  }
98
  body {
99
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
100
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 
101
  min-height: 100vh;
102
  padding: 20px;
103
  }
104
  .container {
105
- max-width: 1200px;
106
  margin: 0 auto;
107
- background: white;
108
  border-radius: 20px;
109
  box-shadow: 0 20px 40px rgba(0,0,0,0.1);
110
- overflow: hidden;
111
- }
112
- .header {
113
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
114
- color: white;
115
  padding: 30px;
116
- text-align: center;
 
 
 
 
 
 
 
 
 
 
117
  }
118
- .header h1 {
119
  font-size: 2.5rem;
120
- margin-bottom: 10px;
121
- font-weight: 700;
122
  }
123
- .header p {
124
  font-size: 1.1rem;
125
- opacity: 0.9;
126
- }
127
- .controls {
128
- padding: 20px 30px;
129
- background: #f8f9fa;
130
- border-bottom: 1px solid #e9ecef;
131
- display: flex;
132
- gap: 15px;
133
- align-items: center;
134
- flex-wrap: wrap;
135
- }
136
- .control-group {
137
- display: flex;
138
- align-items: center;
139
- gap: 8px;
140
- }
141
- .control-group label {
142
- font-weight: 600;
143
- color: #495057;
144
- min-width: 80px;
145
- }
146
- .control-group select,
147
- .control-group input {
148
- padding: 8px 12px;
149
- border: 2px solid #e9ecef;
150
- border-radius: 8px;
151
- font-size: 14px;
152
- transition: border-color 0.3s;
153
- }
154
- .control-group select:focus,
155
- .control-group input:focus {
156
- outline: none;
157
- border-color: #667eea;
158
- }
159
- .chat-container {
160
- height: 500px;
161
- overflow-y: auto;
162
- padding: 20px;
163
- background: #fafbfc;
164
- }
165
- .message {
166
  margin-bottom: 20px;
167
- display: flex;
168
- gap: 15px;
169
  }
170
- .message.user {
171
- flex-direction: row-reverse;
172
- }
173
- .message-avatar {
174
- width: 40px;
175
- height: 40px;
176
- border-radius: 50%;
177
- display: flex;
178
- align-items: center;
179
- justify-content: center;
180
- font-weight: bold;
181
- color: white;
182
- flex-shrink: 0;
183
- }
184
- .message.user .message-avatar {
185
- background: #667eea;
186
- }
187
- .message.assistant .message-avatar {
188
- background: #28a745;
189
- }
190
- .message-content {
191
- background: white;
192
- padding: 15px 20px;
193
- border-radius: 18px;
194
- max-width: 70%;
195
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
196
- line-height: 1.5;
197
- }
198
- .message.user .message-content {
199
- background: #667eea;
200
- color: white;
201
- }
202
- .message.assistant .message-content {
203
- background: white;
204
- color: #333;
205
- }
206
- .input-container {
207
- padding: 20px 30px;
208
- background: white;
209
- border-top: 1px solid #e9ecef;
210
- }
211
- .input-form {
212
- display: flex;
213
- gap: 15px;
214
- }
215
- .input-field {
216
- flex: 1;
217
- padding: 15px 20px;
218
- border: 2px solid #e9ecef;
219
- border-radius: 25px;
220
- font-size: 16px;
221
- transition: border-color 0.3s;
222
- resize: none;
223
- min-height: 50px;
224
- max-height: 120px;
225
- }
226
- .input-field:focus {
227
- outline: none;
228
- border-color: #667eea;
229
  }
230
- .send-button {
231
- padding: 15px 30px;
232
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
233
  color: white;
234
- border: none;
235
- border-radius: 25px;
236
- font-size: 16px;
237
- font-weight: 600;
238
- cursor: pointer;
239
- transition: transform 0.2s;
240
- min-width: 100px;
241
  }
242
- .send-button:hover {
243
- transform: translateY(-2px);
 
 
 
244
  }
245
- .send-button:disabled {
246
- opacity: 0.6;
247
- cursor: not-allowed;
248
- transform: none;
 
249
  }
250
- .status {
251
- text-align: center;
252
- padding: 10px;
253
  font-size: 14px;
254
- color: #6c757d;
255
- }
256
- .status.error {
257
- color: #dc3545;
258
- }
259
- .status.success {
260
- color: #28a745;
261
  }
262
- .typing-indicator {
263
- display: none;
264
- padding: 15px 20px;
265
- background: white;
266
- border-radius: 18px;
267
- color: #6c757d;
268
- font-style: italic;
269
  }
270
  @media (max-width: 768px) {
271
- .controls {
272
- flex-direction: column;
273
- align-items: stretch;
274
  }
275
- .control-group {
276
- justify-content: space-between;
277
- }
278
- .message-content {
279
- max-width: 85%;
280
  }
281
  }
282
  </style>
283
  </head>
284
  <body>
285
  <div class="container">
286
- <div class="header">
287
- <h1>🤖 OpenWebUI</h1>
288
- <p>Chat with your local Ollama models through Hugging Face Spaces</p>
289
- </div>
290
 
291
- <div class="controls">
292
- <div class="control-group">
293
- <label for="model-select">Model:</label>
294
- <select id="model-select">
295
- <option value="">Select a model...</option>
296
- </select>
297
- </div>
298
- <div class="control-group">
299
- <label for="temperature">Temperature:</label>
300
- <input type="range" id="temperature" min="0" max="2" step="0.1" value="0.7">
301
- <span id="temp-value">0.7</span>
302
- </div>
303
- <div class="control-group">
304
- <label for="max-tokens">Max Tokens:</label>
305
- <input type="number" id="max-tokens" min="1" max="4096" value="2048">
306
- </div>
307
  </div>
308
 
309
- <div class="chat-container" id="chat-container">
310
- <div class="message assistant">
311
- <div class="message-avatar">AI</div>
312
- <div class="message-content">
313
- Hello! I'm your AI assistant powered by Ollama. How can I help you today?
314
- </div>
315
- </div>
316
  </div>
317
 
318
- <div class="typing-indicator" id="typing-indicator">
319
- AI is thinking...
 
 
 
 
320
  </div>
321
 
322
- <div class="input-container">
323
- <form class="input-form" id="chat-form">
324
- <textarea
325
- class="input-field"
326
- id="message-input"
327
- placeholder="Type your message here..."
328
- rows="1"
329
- ></textarea>
330
- <button type="submit" class="send-button" id="send-button">
331
- Send
332
- </button>
333
- </form>
334
  </div>
335
 
336
- <div class="status" id="status"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  </div>
338
 
339
  <script>
340
- let conversationHistory = [];
341
-
342
  document.addEventListener('DOMContentLoaded', function() {
343
- loadModels();
344
- setupEventListeners();
345
- autoResizeTextarea();
346
- });
347
-
348
- async function loadModels() {
349
- const modelSelect = document.getElementById('model-select');
350
- modelSelect.innerHTML = '<option value="">Loading models...</option>';
351
-
352
- try {
353
- const response = await fetch('/api/models');
354
- const data = await response.json();
355
-
356
- modelSelect.innerHTML = '<option value="">Select a model...</option>';
357
-
358
- if (data.status === 'success' && data.models.length > 0) {
359
- data.models.forEach(model => {
360
- const option = document.createElement('option');
361
- option.value = model;
362
- option.textContent = model;
363
- if (model === 'gemma-3-270m') {
364
- option.selected = true;
365
- }
366
- modelSelect.appendChild(option);
367
- });
368
- showStatus('Models loaded successfully', 'success');
369
- } else {
370
- modelSelect.innerHTML = '<option value="">No models available</option>';
371
- showStatus('No models available from API', 'error');
372
- }
373
- } catch (error) {
374
- console.error('Error loading models:', error);
375
- modelSelect.innerHTML = '<option value="">No models available</option>';
376
- showStatus('Failed to load models: ' + error.message, 'error');
377
- }
378
- }
379
-
380
- function setupEventListeners() {
381
- document.getElementById('chat-form').addEventListener('submit', handleSubmit);
382
- document.getElementById('temperature').addEventListener('input', function() {
383
- document.getElementById('temp-value').textContent = this.value;
384
  });
385
- document.getElementById('message-input').addEventListener('input', autoResizeTextarea);
386
- }
387
-
388
- function autoResizeTextarea() {
389
- const textarea = document.getElementById('message-input');
390
- textarea.style.height = 'auto';
391
- textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
392
- }
393
-
394
- async function handleSubmit(e) {
395
- e.preventDefault();
396
-
397
- const messageInput = document.getElementById('message-input');
398
- const message = messageInput.value.trim();
399
-
400
- if (!message) return;
401
-
402
- const model = document.getElementById('model-select').value;
403
- const temperature = parseFloat(document.getElementById('temperature').value);
404
- const maxTokens = parseInt(document.getElementById('max-tokens').value);
405
-
406
- if (!model) {
407
- showStatus('Please select a model', 'error');
408
- return;
409
- }
410
-
411
- addMessage(message, 'user');
412
- messageInput.value = '';
413
- autoResizeTextarea();
414
- showTypingIndicator(true);
415
-
416
- try {
417
- const response = await fetch('/api/chat', {
418
- method: 'POST',
419
- headers: { 'Content-Type': 'application/json' },
420
- body: JSON.stringify({ model, prompt: message, temperature, max_tokens: maxTokens })
421
- });
422
- const data = await response.json();
423
-
424
- showTypingIndicator(false);
425
-
426
- if (data.status === 'success') {
427
- addMessage(data.response, 'assistant');
428
- showStatus(`Response generated using ${model}`, 'success');
429
- } else {
430
- addMessage('Sorry, I encountered an error while processing your request.', 'assistant');
431
- showStatus(`Error: ${data.message}`, 'error');
432
- }
433
- } catch (error) {
434
- showTypingIndicator(false);
435
- addMessage('Sorry, I encountered a network error.', 'assistant');
436
- showStatus('Network error: ' + error.message, 'error');
437
- }
438
- }
439
-
440
- function addMessage(content, sender) {
441
- const chatContainer = document.getElementById('chat-container');
442
- const messageDiv = document.createElement('div');
443
- messageDiv.className = `message ${sender}`;
444
-
445
- const avatar = document.createElement('div');
446
- avatar.className = 'message-avatar';
447
- avatar.textContent = sender === 'user' ? 'U' : 'AI';
448
-
449
- const messageContent = document.createElement('div');
450
- messageContent.className = 'message-content';
451
- messageContent.textContent = content;
452
-
453
- messageDiv.appendChild(avatar);
454
- messageDiv.appendChild(messageContent);
455
- chatContainer.appendChild(messageDiv);
456
- chatContainer.scrollTop = chatContainer.scrollHeight;
457
-
458
- conversationHistory.push({ role: sender, content: content });
459
- }
460
-
461
- function showTypingIndicator(show) {
462
- const indicator = document.getElementById('typing-indicator');
463
- indicator.style.display = show ? 'block' : 'none';
464
- if (show) {
465
- const chatContainer = document.getElementById('chat-container');
466
- chatContainer.scrollTop = chatContainer.scrollHeight;
467
  }
468
- }
469
-
470
- function showStatus(message, type = '') {
471
- const statusDiv = document.getElementById('status');
472
- statusDiv.textContent = message;
473
- statusDiv.className = `status ${type}`;
474
- setTimeout(() => {
475
- statusDiv.textContent = '';
476
- statusDiv.className = 'status';
477
- }, 5000);
478
- }
479
  </script>
480
  </body>
481
  </html>
@@ -483,31 +276,12 @@ HTML_TEMPLATE = '''
483
 
484
  @app.route('/')
485
  def home():
486
- """Main chat interface."""
487
- return render_template_string(HTML_TEMPLATE, ollama_base_url=OLLAMA_BASE_URL, default_model=ALLOWED_MODELS)
488
-
489
- @app.route('/api/chat', methods=['POST'])
490
- def chat():
491
- """Chat API endpoint."""
492
- try:
493
- data = request.get_json()
494
- if not data or 'prompt' not in data or 'model' not in data:
495
- return jsonify({"status": "error", "message": "Prompt and model are required"}), 400
496
-
497
- prompt = data['prompt']
498
- model = data['model']
499
- temperature = data.get('temperature', TEMPERATURE)
500
- max_tokens = data.get('max_tokens', MAX_TOKENS)
501
-
502
- result = ollama_manager.generate(model, prompt, temperature=temperature, max_tokens=max_tokens)
503
- return jsonify(result), 200 if result["status"] == "success" else 500
504
- except Exception as e:
505
- logging.error(f"Chat endpoint error: {e}")
506
- return jsonify({"status": "error", "message": str(e)}), 500
507
 
508
  @app.route('/api/models', methods=['GET'])
509
- def get_models():
510
- """Get available models."""
511
  try:
512
  models = ollama_manager.list_models()
513
  return jsonify({
@@ -519,23 +293,70 @@ def get_models():
519
  logging.error(f"Models endpoint error: {e}")
520
  return jsonify({"status": "error", "message": str(e)}), 500
521
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
  @app.route('/health', methods=['GET'])
523
  def health_check():
524
  """Health check endpoint."""
525
  try:
526
- ollama_health = ollama_manager.health_check()
 
527
  return jsonify({
528
  "status": "healthy",
529
- "ollama_api": ollama_health,
 
530
  "timestamp": time.time()
531
  })
532
  except Exception as e:
533
  logging.error(f"Health check endpoint error: {e}")
534
  return jsonify({
535
  "status": "unhealthy",
 
536
  "error": str(e),
537
  "timestamp": time.time()
538
- }), 500
539
 
540
  if __name__ == '__main__':
541
  app.run(host='0.0.0.0', port=7860, debug=False)
 
1
  # app.py
2
+ from flask import Flask, request, jsonify, Response
3
  import os
4
  import requests
5
  import json
 
11
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
12
 
13
  # Configuration
14
+ OLLAMA_BASE_URL = os.getenv('OLLAMA_BASE_URL', 'https://huggingface.co/spaces/tommytracx/ollama-api')
15
  ALLOWED_MODELS = os.getenv('ALLOWED_MODELS', 'llama2,llama2:13b,llama2:70b,codellama,neural-chat,gemma-3-270m').split(',')
16
  MAX_TOKENS = int(os.getenv('MAX_TOKENS', '2048'))
17
  TEMPERATURE = float(os.getenv('TEMPERATURE', '0.7'))
 
43
  """Return the list of available models."""
44
  return self.available_models
45
 
46
+ def pull_model(self, model_name: str) -> Dict[str, Any]:
47
+ """Pull a model from Ollama."""
48
+ if model_name not in ALLOWED_MODELS:
49
+ return {"status": "error", "message": f"Model {model_name} not in allowed list"}
50
+
51
+ try:
52
+ response = requests.post(f"{self.base_url}/api/pull", json={"name": model_name}, timeout=300)
53
+ response.raise_for_status()
54
+ return {"status": "success", "model": model_name}
55
+ except Exception as e:
56
+ logging.error(f"Error pulling model {model_name}: {e}")
57
+ return {"status": "error", "message": str(e)}
58
+
59
+ def generate(self, model_name: str, prompt: str, stream: bool = False, **kwargs) -> Any:
60
+ """Generate text using a model, with optional streaming."""
61
  if model_name not in self.available_models:
62
  return {"status": "error", "message": f"Model {model_name} not available"}
63
 
 
65
  payload = {
66
  "model": model_name,
67
  "prompt": prompt,
68
+ "stream": stream,
69
  **kwargs
70
  }
71
+ if stream:
72
+ response = requests.post(f"{self.base_url}/api/generate", json=payload, stream=True, timeout=120)
73
+ response.raise_for_status()
74
+ return response
75
+ else:
76
+ response = requests.post(f"{self.base_url}/api/generate", json=payload, timeout=120)
77
+ response.raise_for_status()
78
+ data = response.json()
79
+ return {
80
+ "status": "success",
81
+ "response": data.get('response', ''),
82
+ "model": model_name,
83
+ "usage": data.get('usage', {})
84
+ }
85
  except Exception as e:
86
  logging.error(f"Error generating response: {e}")
87
  return {"status": "error", "message": str(e)}
 
 
 
 
 
 
 
 
 
 
88
 
89
  # Initialize Ollama manager
90
  ollama_manager = OllamaManager(OLLAMA_BASE_URL)
91
 
92
+ # HTML template for the home page with improved UI
93
  HTML_TEMPLATE = '''
94
  <!DOCTYPE html>
95
  <html lang="en">
96
  <head>
97
  <meta charset="UTF-8">
98
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
99
+ <title>Ollama API Space</title>
100
  <style>
101
+ :root {
102
+ --primary-color: #667eea;
103
+ --secondary-color: #764ba2;
104
+ --text-color: #333;
105
+ --bg-color: #fafbfc;
106
+ --border-color: #e9ecef;
107
+ }
108
+ .dark-mode {
109
+ --primary-color: #3b4a8c;
110
+ --secondary-color: #4a2e6b;
111
+ --text-color: #f0f0f0;
112
+ --bg-color: #1a1a1a;
113
+ --border-color: #4a4a4a;
114
+ }
115
  * {
116
  margin: 0;
117
  padding: 0;
 
119
  }
120
  body {
121
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
122
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
123
+ color: var(--text-color);
124
  min-height: 100vh;
125
  padding: 20px;
126
  }
127
  .container {
128
+ max-width: 800px;
129
  margin: 0 auto;
130
+ background: var(--bg-color);
131
  border-radius: 20px;
132
  box-shadow: 0 20px 40px rgba(0,0,0,0.1);
 
 
 
 
 
133
  padding: 30px;
134
+ position: relative;
135
+ }
136
+ .theme-toggle {
137
+ position: absolute;
138
+ top: 10px;
139
+ right: 10px;
140
+ background: none;
141
+ border: none;
142
+ cursor: pointer;
143
+ font-size: 1.2rem;
144
+ color: var(--text-color);
145
  }
146
+ h1 {
147
  font-size: 2.5rem;
148
+ margin-bottom: 20px;
149
+ text-align: center;
150
  }
151
+ p {
152
  font-size: 1.1rem;
153
+ line-height: 1.6;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  margin-bottom: 20px;
 
 
155
  }
156
+ .endpoint {
157
+ background: var(--border-color);
158
+ padding: 15px;
159
+ margin: 10px 0;
160
+ border-radius: 8px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  }
162
+ .method {
163
+ background: var(--primary-color);
 
164
  color: white;
165
+ padding: 4px 10px;
166
+ border-radius: 4px;
167
+ font-size: 14px;
168
+ margin-right: 10px;
 
 
 
169
  }
170
+ .url {
171
+ font-family: monospace;
172
+ background: var(--bg-color);
173
+ padding: 4px 8px;
174
+ border-radius: 4px;
175
  }
176
+ pre {
177
+ background: var(--border-color);
178
+ padding: 15px;
179
+ border-radius: 8px;
180
+ overflow-x: auto;
181
  }
182
+ code {
183
+ font-family: monospace;
 
184
  font-size: 14px;
 
 
 
 
 
 
 
185
  }
186
+ .dark-mode pre, .dark-mode .endpoint {
187
+ background: #2a2a2a;
 
 
 
 
 
188
  }
189
  @media (max-width: 768px) {
190
+ .container {
191
+ padding: 20px;
 
192
  }
193
+ h1 {
194
+ font-size: 2rem;
 
 
 
195
  }
196
  }
197
  </style>
198
  </head>
199
  <body>
200
  <div class="container">
201
+ <button class="theme-toggle" id="theme-toggle">🌙</button>
202
+ <h1>🚀 Ollama API Space</h1>
203
+ <p>This Space provides a robust API for managing and interacting with Ollama models, optimized for integration with OpenWebUI and other clients.</p>
 
204
 
205
+ <h2>Available Endpoints</h2>
206
+
207
+ <div class="endpoint">
208
+ <span class="method">GET</span> <span class="url">/api/models</span>
209
+ <p>List all available models filtered by ALLOWED_MODELS.</p>
210
+ <p><strong>Response:</strong> <code>{"status": "success", "models": [...], "count": N}</code></p>
 
 
 
 
 
 
 
 
 
 
211
  </div>
212
 
213
+ <div class="endpoint">
214
+ <span class="method">POST</span> <span class="url">/api/models/pull</span>
215
+ <p>Pull a model from Ollama, restricted to ALLOWED_MODELS.</p>
216
+ <p><strong>Body:</strong> <code>{"name": "model_name"}</code></p>
217
+ <p><strong>Response:</strong> <code>{"status": "success", "model": "model_name"}</code></p>
 
 
218
  </div>
219
 
220
+ <div class="endpoint">
221
+ <span class="method">POST</span> <span class="url">/api/generate</span>
222
+ <p>Generate text using a model, with optional streaming.</p>
223
+ <p><strong>Body:</strong> <code>{"model": "model_name", "prompt": "your prompt", "stream": boolean}</code></p>
224
+ <p><strong>Response (non-streaming):</strong> <code>{"status": "success", "response": "...", "model": "...", "usage": {...}}</code></p>
225
+ <p><strong>Response (streaming):</strong> Stream of JSON objects</p>
226
  </div>
227
 
228
+ <div class="endpoint">
229
+ <span class="method">GET</span> <span class="url">/health</span>
230
+ <p>Health check endpoint for the API and Ollama connection.</p>
231
+ <p><strong>Response:</strong> <code>{"status": "healthy", "ollama_connection": "connected", "available_models": N}</code></p>
 
 
 
 
 
 
 
 
232
  </div>
233
 
234
+ <h2>Usage Examples</h2>
235
+ <p>Use this API with OpenWebUI or any REST client. Ensure models are in ALLOWED_MODELS: {{ allowed_models }}.</p>
236
+
237
+ <h3>cURL Examples</h3>
238
+ <pre>
239
+ # List models
240
+ curl {{ ollama_base_url }}/api/models
241
+
242
+ # Pull a model
243
+ curl -X POST {{ ollama_base_url }}/api/models/pull \
244
+ -H "Content-Type: application/json" \
245
+ -d '{"name": "gemma-3-270m"}'
246
+
247
+ # Generate text (non-streaming)
248
+ curl -X POST {{ ollama_base_url }}/api/generate \
249
+ -H "Content-Type: application/json" \
250
+ -d '{"model": "gemma-3-270m", "prompt": "Write a Python script"}'
251
+
252
+ # Generate text (streaming)
253
+ curl -X POST {{ ollama_base_url }}/api/generate \
254
+ -H "Content-Type: application/json" \
255
+ -d '{"model": "gemma-3-270m", "prompt": "Write a Python script", "stream": true}'
256
+ </pre>
257
  </div>
258
 
259
  <script>
 
 
260
  document.addEventListener('DOMContentLoaded', function() {
261
+ const themeToggle = document.getElementById('theme-toggle');
262
+ themeToggle.addEventListener('click', function() {
263
+ document.body.classList.toggle('dark-mode');
264
+ themeToggle.textContent = document.body.classList.contains('dark-mode') ? '☀️' : '🌙';
265
+ localStorage.setItem('theme', document.body.classList.contains('dark-mode') ? 'dark' : 'light');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  });
267
+ if (localStorage.getItem('theme') === 'dark') {
268
+ document.body.classList.add('dark-mode');
269
+ themeToggle.textContent = '☀️';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  }
271
+ });
 
 
 
 
 
 
 
 
 
 
272
  </script>
273
  </body>
274
  </html>
 
276
 
277
  @app.route('/')
278
  def home():
279
+ """Home page with API documentation."""
280
+ return render_template_string(HTML_TEMPLATE, ollama_base_url=OLLAMA_BASE_URL, allowed_models=', '.join(ALLOWED_MODELS))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
 
282
  @app.route('/api/models', methods=['GET'])
283
+ def list_models():
284
+ """List all available models."""
285
  try:
286
  models = ollama_manager.list_models()
287
  return jsonify({
 
293
  logging.error(f"Models endpoint error: {e}")
294
  return jsonify({"status": "error", "message": str(e)}), 500
295
 
296
+ @app.route('/api/models/pull', methods=['POST'])
297
+ def pull_model():
298
+ """Pull a model from Ollama."""
299
+ try:
300
+ data = request.get_json()
301
+ if not data or 'name' not in data:
302
+ return jsonify({"status": "error", "message": "Model name is required"}), 400
303
+
304
+ model_name = data['name']
305
+ if model_name not in ALLOWED_MODELS:
306
+ return jsonify({"status": "error", "message": f"Model {model_name} not in allowed list"}), 400
307
+
308
+ result = ollama_manager.pull_model(model_name)
309
+ return jsonify(result), 200 if result["status"] == "success" else 500
310
+ except Exception as e:
311
+ logging.error(f"Pull model endpoint error: {e}")
312
+ return jsonify({"status": "error", "message": str(e)}), 500
313
+
314
+ @app.route('/api/generate', methods=['POST'])
315
+ def generate_text():
316
+ """Generate text using a model, with optional streaming."""
317
+ try:
318
+ data = request.get_json()
319
+ if not data or 'model' not in data or 'prompt' not in data:
320
+ return jsonify({"status": "error", "message": "Model name and prompt are required"}), 400
321
+
322
+ model_name = data['model']
323
+ prompt = data['prompt']
324
+ stream = data.get('stream', False)
325
+ kwargs = {k: v for k in data if k not in ['model', 'prompt', 'stream']}
326
+
327
+ result = ollama_manager.generate(model_name, prompt, stream=stream, **kwargs)
328
+
329
+ if stream and isinstance(result, requests.Response):
330
+ def generate_stream():
331
+ for chunk in result.iter_content(chunk_size=None):
332
+ yield chunk
333
+ return Response(generate_stream(), content_type='application/json')
334
+ else:
335
+ return jsonify(result), 200 if result["status"] == "success" else 500
336
+ except Exception as e:
337
+ logging.error(f"Generate endpoint error: {e}")
338
+ return jsonify({"status": "error", "message": str(e)}), 500
339
+
340
  @app.route('/health', methods=['GET'])
341
  def health_check():
342
  """Health check endpoint."""
343
  try:
344
+ response = requests.get(f"{OLLAMA_BASE_URL}/api/tags", timeout=5)
345
+ response.raise_for_status()
346
  return jsonify({
347
  "status": "healthy",
348
+ "ollama_connection": "connected",
349
+ "available_models": len(ollama_manager.available_models),
350
  "timestamp": time.time()
351
  })
352
  except Exception as e:
353
  logging.error(f"Health check endpoint error: {e}")
354
  return jsonify({
355
  "status": "unhealthy",
356
+ "ollama_connection": "failed",
357
  "error": str(e),
358
  "timestamp": time.time()
359
+ }), 503
360
 
361
  if __name__ == '__main__':
362
  app.run(host='0.0.0.0', port=7860, debug=False)