Skip to content

Commit 70dfdf9

Browse files
authored
Added support for uploading image files (#6)
Note: only passes the image files to ollama, not llama.cpp models.
1 parent d257326 commit 70dfdf9

File tree

5 files changed

+54
-27
lines changed

5 files changed

+54
-27
lines changed

ai_server/server.py

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from __future__ import annotations
2+
3+
import base64
14
import glob
25
import os
36
import subprocess
@@ -31,24 +34,31 @@
3134
)
3235

3336

34-
def _build_messages(content: str, system_prompt: Optional[str] = None) -> list:
37+
def _build_messages(content: str, system_prompt: Optional[str] = None, image_files: Optional[list] = None) -> list:
3538
"""Build messages list with optional system prompt."""
3639
messages = []
3740
if system_prompt:
3841
messages.append({'role': 'system', 'content': system_prompt})
3942
messages.append({'role': 'user', 'content': content})
43+
44+
if image_files:
45+
messages[-1]["images"] = [base64.b64encode(image_file.read()).decode("utf-8") for image_file in image_files]
4046
return messages
4147

4248

4349
def chat_with_llama_server_http(
44-
model: str, content: str, system_prompt: Optional[str] = None, timeout: int = 300
50+
model: str,
51+
content: str,
52+
system_prompt: Optional[str] = None,
53+
timeout: int = 300,
54+
image_files: Optional[list] = None,
4555
) -> str:
4656
"""Handle chat using llama-server HTTP API."""
4757
if not LLAMA_SERVER_URL:
4858
raise Exception("LLAMA_SERVER_URL environment variable not set")
4959

5060
try:
51-
messages = _build_messages(content, system_prompt)
61+
messages = _build_messages(content, system_prompt, image_files=[]) # TODO: Pass image files
5262

5363
response = requests.post(
5464
f'{LLAMA_SERVER_URL}/v1/chat/completions',
@@ -84,15 +94,23 @@ def is_llamacpp_available(model: str) -> bool:
8494
return resolve_model_path(model) is not None
8595

8696

87-
def chat_with_ollama(model: str, content: str, system_prompt: Optional[str] = None) -> str:
97+
def chat_with_ollama(
98+
model: str, content: str, system_prompt: Optional[str] = None, image_files: Optional[list] = None
99+
) -> str:
88100
"""Handle chat using ollama."""
89-
messages = _build_messages(content, system_prompt)
101+
messages = _build_messages(content, system_prompt, image_files)
90102

91103
response = ollama.chat(model=model, messages=messages, stream=False)
92104
return response.message.content
93105

94106

95-
def chat_with_llamacpp(model: str, content: str, system_prompt: Optional[str] = None, timeout: int = 300) -> str:
107+
def chat_with_llamacpp(
108+
model: str,
109+
content: str,
110+
system_prompt: Optional[str] = None,
111+
timeout: int = 300,
112+
image_files: Optional[list] = None,
113+
) -> str:
96114
"""Handle chat using llama.cpp CLI."""
97115
model_path = resolve_model_path(model)
98116

@@ -105,6 +123,9 @@ def chat_with_llamacpp(model: str, content: str, system_prompt: Optional[str] =
105123
if system_prompt:
106124
cmd.extend(['--system-prompt', system_prompt])
107125

126+
if image_files:
127+
pass # TODO: pass image files
128+
108129
try:
109130
result = subprocess.run(cmd, capture_output=True, text=False, timeout=timeout, check=True)
110131

@@ -125,20 +146,26 @@ def chat_with_llamacpp(model: str, content: str, system_prompt: Optional[str] =
125146
raise Exception("Llama.cpp CLI not found")
126147

127148

128-
def chat_with_model(model: str, content: str, llama_mode: str = "cli", system_prompt: Optional[str] = None) -> str:
149+
def chat_with_model(
150+
model: str,
151+
content: str,
152+
llama_mode: str = "cli",
153+
system_prompt: Optional[str] = None,
154+
image_files: Optional[list] = None,
155+
) -> str:
129156
"""Route chat request based on llama_mode: server (external), cli, or ollama fallback; and with optional system prompt."""
130157
if is_llamacpp_available(model):
131158
if llama_mode == "server":
132159
if not LLAMA_SERVER_URL:
133160
raise Exception("LLAMA_SERVER_URL environment variable not set for server mode")
134-
return chat_with_llama_server_http(model, content, system_prompt)
161+
return chat_with_llama_server_http(model, content, system_prompt, image_files)
135162
elif llama_mode == "cli":
136-
return chat_with_llamacpp(model, content, system_prompt)
163+
return chat_with_llamacpp(model, content, system_prompt, image_files)
137164
else:
138165
raise ValueError(f"Invalid llama_mode: '{llama_mode}'. Valid options are 'server' or 'cli'.")
139166
else:
140167
# Model not available in llama.cpp, use ollama
141-
return chat_with_ollama(model, content, system_prompt)
168+
return chat_with_ollama(model, content, system_prompt, image_files)
142169

143170

144171
def authenticate() -> str:
@@ -158,16 +185,16 @@ def authenticate() -> str:
158185
def chat():
159186
"""Handle chat request with optional llama_mode and system prompt parameters."""
160187
authenticate()
161-
params = request.get_json()
162-
model = params.get('model', DEFAULT_MODEL)
163-
content = params.get('content', '')
164-
llama_mode = params.get('llama_mode', 'cli')
165-
system_prompt = params.get('system_prompt')
188+
model = request.form.get('model', DEFAULT_MODEL)
189+
content = request.form.get('content', '')
190+
llama_mode = request.form.get('llama_mode', 'cli')
191+
system_prompt = request.form.get('system_prompt')
192+
image_files = list(request.files.values())
166193

167194
if not content.strip():
168195
abort(400, description='Missing prompt content')
169196

170-
response_content = chat_with_model(model, content, llama_mode, system_prompt)
197+
response_content = chat_with_model(model, content, llama_mode, system_prompt, image_files)
171198
return jsonify(response_content)
172199

173200

test/test_cli_mode.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def test_cli_mode_uses_llamacpp_when_available(self):
108108

109109
assert result == "CLI response from DeepSeek V3"
110110
self.mock_available.assert_called_once_with(TEST_LLAMACPP_MODEL)
111-
self.mock_chat_llamacpp.assert_called_once_with(TEST_LLAMACPP_MODEL, 'Write a function', None)
111+
self.mock_chat_llamacpp.assert_called_once_with(TEST_LLAMACPP_MODEL, 'Write a function', None, None)
112112

113113
def test_cli_mode_fallback_to_ollama_when_unavailable(self):
114114
"""Test CLI mode falls back to ollama when model not available in llama.cpp."""
@@ -119,7 +119,7 @@ def test_cli_mode_fallback_to_ollama_when_unavailable(self):
119119

120120
assert result == "Ollama response from DeepSeek Coder"
121121
self.mock_available.assert_called_once_with(TEST_OLLAMA_MODEL)
122-
self.mock_chat_ollama.assert_called_once_with(TEST_OLLAMA_MODEL, 'Help with coding', None)
122+
self.mock_chat_ollama.assert_called_once_with(TEST_OLLAMA_MODEL, 'Help with coding', None, None)
123123

124124
def test_default_mode_is_cli(self):
125125
"""Test that default mode is CLI when no llama_mode specified."""
@@ -130,7 +130,7 @@ def test_default_mode_is_cli(self):
130130

131131
assert result == "Default CLI mode response"
132132
self.mock_available.assert_called_once_with(TEST_LLAMACPP_MODEL)
133-
self.mock_chat_llamacpp.assert_called_once_with(TEST_LLAMACPP_MODEL, 'Help me', None)
133+
self.mock_chat_llamacpp.assert_called_once_with(TEST_LLAMACPP_MODEL, 'Help me', None, None)
134134

135135

136136
class TestCLIModeIntegration:

test/test_server_mode.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def test_server_mode_uses_llamacpp_when_available(self):
111111

112112
assert result == "Server response from DeepSeek V3"
113113
self.mock_available.assert_called_once_with(TEST_LLAMACPP_MODEL)
114-
self.mock_chat_server.assert_called_once_with(TEST_LLAMACPP_MODEL, 'Explain code', None)
114+
self.mock_chat_server.assert_called_once_with(TEST_LLAMACPP_MODEL, 'Explain code', None, None)
115115

116116
def test_server_mode_fallback_to_ollama_when_unavailable(self):
117117
"""Test server mode falls back to ollama when model not available in llama.cpp."""
@@ -122,7 +122,7 @@ def test_server_mode_fallback_to_ollama_when_unavailable(self):
122122

123123
assert result == "Ollama fallback response"
124124
self.mock_available.assert_called_once_with(TEST_OLLAMA_MODEL)
125-
self.mock_chat_ollama.assert_called_once_with(TEST_OLLAMA_MODEL, 'Debug code', None)
125+
self.mock_chat_ollama.assert_called_once_with(TEST_OLLAMA_MODEL, 'Debug code', None, None)
126126

127127
def test_server_mode_requires_server_url(self):
128128
"""Test server mode requires LLAMA_SERVER_URL to be set."""

test/test_system_prompt.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,4 @@ def test_chat_with_model_routing(self, mock_available, mock_chat):
7777
mock_chat.return_value = "result"
7878

7979
chat_with_model(TEST_MODEL, TEST_USER_CONTENT, 'cli', TEST_SYSTEM_PROMPT)
80-
mock_chat.assert_called_once_with(TEST_MODEL, TEST_USER_CONTENT, TEST_SYSTEM_PROMPT)
80+
mock_chat.assert_called_once_with(TEST_MODEL, TEST_USER_CONTENT, TEST_SYSTEM_PROMPT, None)

test/test_system_prompt_api.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ def test_api_with_system_prompt(self, mock_chat, mock_redis, client):
3434
response = client.post(
3535
'/chat',
3636
headers={'X-API-KEY': 'test-key'},
37-
json={'model': TEST_MODEL, 'content': TEST_USER_CONTENT, 'system_prompt': TEST_SYSTEM_PROMPT},
37+
data={'model': TEST_MODEL, 'content': TEST_USER_CONTENT, 'system_prompt': TEST_SYSTEM_PROMPT},
3838
)
3939

4040
assert response.status_code == 200
4141

42-
mock_chat.assert_called_once_with(TEST_MODEL, TEST_USER_CONTENT, 'cli', TEST_SYSTEM_PROMPT)
42+
mock_chat.assert_called_once_with(TEST_MODEL, TEST_USER_CONTENT, 'cli', TEST_SYSTEM_PROMPT, [])
4343

4444
@patch('ai_server.server.REDIS_CONNECTION')
4545
@patch('ai_server.server.chat_with_model')
@@ -49,12 +49,12 @@ def test_api_without_system_prompt(self, mock_chat, mock_redis, client):
4949
mock_chat.return_value = "def function(): pass"
5050

5151
response = client.post(
52-
'/chat', headers={'X-API-KEY': 'test-key'}, json={'model': TEST_MODEL, 'content': TEST_USER_CONTENT}
52+
'/chat', headers={'X-API-KEY': 'test-key'}, data={'model': TEST_MODEL, 'content': TEST_USER_CONTENT}
5353
)
5454

5555
assert response.status_code == 200
5656

57-
mock_chat.assert_called_once_with(TEST_MODEL, TEST_USER_CONTENT, 'cli', None)
57+
mock_chat.assert_called_once_with(TEST_MODEL, TEST_USER_CONTENT, 'cli', None, [])
5858

5959
@patch('ai_server.server.REDIS_CONNECTION')
6060
def test_api_authentication_still_required(self, mock_redis, client):
@@ -64,7 +64,7 @@ def test_api_authentication_still_required(self, mock_redis, client):
6464
response = client.post(
6565
'/chat',
6666
headers={'X-API-KEY': 'invalid-key'},
67-
json={'model': TEST_MODEL, 'content': TEST_USER_CONTENT, 'system_prompt': TEST_SYSTEM_PROMPT},
67+
data={'model': TEST_MODEL, 'content': TEST_USER_CONTENT, 'system_prompt': TEST_SYSTEM_PROMPT},
6868
)
6969

7070
assert response.status_code == 500

0 commit comments

Comments
 (0)