Skip to content

Commit 87d31d7

Browse files
committed
API updates for ollama
1 parent e053f77 commit 87d31d7

File tree

6 files changed

+193
-29
lines changed

6 files changed

+193
-29
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ This project is the backend API for interacting locally with a text AI model, in
88

99
## Setup
1010

11+
0. Have Docker installed on your system
1112
1. Clone repo + composer install vendors
1213
2. Install Laravel Sail + in terminal in repo base `sail up -d`
1314
3. Prepare the DB with: `sail artisan migrate`
1415

16+
WARNING: the AI model might use at least 12 GB of RAM, so please have more on your system.
17+
1518
## API Endpoints Docs
1619

1720
Your local API Docs URL [here](http://localhost/docs/api#/)

app/Http/Controllers/Api/v1/OllamaController.php

+103-14
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use App\Services\AIModelService;
1010
use Http;
1111
use Storage;
12+
use Symfony\Component\HttpFoundation\StreamedResponse;
1213
use Log;
1314

1415
class OllamaController extends Controller
@@ -32,12 +33,19 @@ public function pullModel(Request $request)
3233
*
3334
* @example mistral
3435
*/
35-
'name' => 'in:mistral',
36+
'name' => 'in:'.config('ai.ollama_allowed_models'),
37+
38+
/**
39+
* Stream the pull request.
40+
*
41+
* @example true
42+
*/
43+
'stream' => 'bool',
3644
]);
3745

38-
$this->aiModelService->pullModel(name: $request->input('name'));
39-
40-
return response([])->setStatusCode(Response::HTTP_NO_CONTENT);
46+
$response = $this->aiModelService->pullModel(name: $request->input('name'), stream: $request->input('stream') ? true : false);
47+
48+
return $response;
4149
}
4250

4351
/**
@@ -81,49 +89,130 @@ public function showModel(Request $request)
8189

8290
return response()->json($response->json())->setStatusCode($response->status());
8391
}
84-
8592

8693
/**
87-
* Show Pulled Models
94+
* Create Model
8895
*
8996
* @unauthenticated
9097
*/
91-
public function showModels(Request $request)
98+
public function createModel(Request $request)
99+
{
100+
$request->validate([
101+
/**
102+
* From Model (the name).
103+
*
104+
* @example mistral
105+
*/
106+
'from_model' => 'required|string',
107+
108+
/**
109+
* Model name (new model name).
110+
*
111+
* @example mistral-houses
112+
*/
113+
'model_name' => 'required|string',
114+
115+
/**
116+
* Temperature from 0.1 to 1 (0.1 neing the most precise, 1.0 being the most creative).
117+
*
118+
* @example 0.7
119+
*/
120+
'temperature' => 'required|decimal:1|between:0.1,1',
121+
122+
/**
123+
* Instructions (instructions to the new model).
124+
*
125+
* @example You are a real estate assistant with built-in knowledge of our properties. Use the provided knowledge base to answer questions accurately.
126+
*/
127+
'instructions' => 'required|string',
128+
129+
/**
130+
* Training data .
131+
*
132+
* @example Available properties:\n1. 123 Oak St - $450,000, 3 bed, 2 bath, 2000 sqft, Type: Single Family, Status: Available\nModern home with updated kitchen and large backyard.\n\n2. 456 Maple Ave - $380,000, 2 bed, 2 bath, 1500 sqft, Type: Condo, Status: Available\nLuxury condo in downtown area with parking.\n\n
133+
*/
134+
'training_data' => 'required|string',
135+
]);
136+
137+
$response = $this->aiModelService->createModel(
138+
fromModel: $request->input('from_model'),
139+
modelName: $request->input('model_name'),
140+
temperature: $request->input('temperature'),
141+
system: $request->input('instructions'),
142+
trainingData: $request->input('training_data')
143+
);
144+
145+
return response()->json($response->json())->setStatusCode($response->status());
146+
}
147+
148+
/**
149+
* List of Models
150+
*
151+
* @unauthenticated
152+
*/
153+
public function indexModels(Request $request)
92154
{
93-
94155
$response = $this->aiModelService->models();
95156

96157
return response(['response' => $response->json()])->setStatusCode($response->status());
97158
}
98159

160+
/**
161+
* List of Tags
162+
*
163+
* @unauthenticated
164+
*/
165+
public function indexTags(Request $request)
166+
{
167+
168+
$response = $this->aiModelService->tags();
169+
170+
return response(['response' => $response->json()])->setStatusCode($response->status());
171+
}
172+
99173
/**
100174
* Generate Text
101175
*
102176
* @unauthenticated
103177
*/
104-
public function generateText(Request $request)
178+
public function generateText(Request $request) : Response|StreamedResponse
105179
{
106180
$request->validate([
107181
/**
108182
* Model.
109183
*
110184
* @example mistral
111185
*/
112-
'model' => 'in:mistral',
186+
'model' => 'required|string',
113187

114188
/**
115189
* Prompt.
116190
*
117191
* @example How far is the moon?
118192
*/
119-
'prompt' => 'string',
193+
'prompt' => 'required|string',
194+
195+
/**
196+
* Stream the request.
197+
*
198+
* @example true
199+
*/
200+
'stream' => 'bool',
120201
]);
121202

203+
$stream = $request->input('stream') ? true : false;
204+
122205
$response = $this->aiModelService->generate(
123206
model: $request->input('model'),
124-
prompt: $request->input('prompt')
207+
prompt: $request->input('prompt'),
208+
stream: $stream
125209
);
126-
127-
return response(['response' => $response->json()])->setStatusCode($response->status());
210+
211+
if(!$stream)
212+
{
213+
return response(['response' => $response->json()])->setStatusCode($response->status());
214+
}
215+
216+
return $response;
128217
}
129218
}

app/Services/AIModelService.php

+82-13
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
namespace App\Services;
44

5-
// 6️⃣ /api/create Create a custom model
6-
// 8️⃣ /api/push Upload a model
75
use Illuminate\Http\Client\Response;
86
use Illuminate\Support\Facades\Http;
7+
use Symfony\Component\HttpFoundation\StreamedResponse;
8+
use Storage;
9+
use Log;
910

1011
class AIModelService
1112
{
@@ -14,13 +15,47 @@ public function __construct(private string $clientPath = '')
1415
$this->clientPath = config('ai.text_client_path');
1516
}
1617

17-
public function pullModel(string $name): void
18+
public function pullModel(string $name, bool $stream = true): Response|StreamedResponse
1819
{
19-
//TODO should stream, stream: true
20-
$response = Http::timeout(60)->post($this->clientPath.'/api/pull', [
21-
'name' => $name,
20+
//if stream is disabled
21+
if(!$stream)
22+
{
23+
24+
$response = Http::timeout(60)->post($this->clientPath.'/api/pull', [
25+
'name' => $name,
26+
'stream' => false
27+
]);
28+
29+
return $response;
30+
}
31+
32+
return $this->streamCall(fn() => Http::withOptions(['stream' => true])
33+
->post($this->clientPath.'/api/pull', ['name' => $name])
34+
->body()
35+
);
36+
}
37+
38+
/**
39+
* string $name e.g. mistral-houses
40+
* string $system e.g. You are a real estate assistant with built-in knowledge of our properties. Use the provided knowledge base to answer questions accurately.
41+
* string $trainingData e.g. Available properties:\n1. 123 Oak St - $450,000, 3 bed, 2 bath, 2000 sqft, Type: Single Family, Status: Available\nModern home with updated kitchen and large backyard.\n\n2. 456 Maple Ave - $380,000, 2 bed, 2 bath, 1500 sqft, Type: Condo, Status: Available\nLuxury condo in downtown area with parking.\n\n
42+
*/
43+
public function createModel(string $fromModel, string $modelName, float $temperature, string $system, string $trainingData): Response
44+
{
45+
$response = Http::timeout(60)->post($this->clientPath.'/api/create', [
46+
'name' => $modelName,
47+
'from' => $fromModel,
48+
'system' => $system,
49+
'template' => "{{ .System }}\n\nKnowledge Base:\n{{ .PropertyData }}\n\nQuestion: {{ .Prompt }}",
50+
'parameter'=> [
51+
'temperature' => $temperature,
52+
'num_ctx' => 4096
53+
],
54+
'propertyData'=> $trainingData,
2255
'stream' => false
2356
]);
57+
58+
return $response;
2459
}
2560

2661
public function deleteModel(string $name): Response
@@ -43,15 +78,28 @@ public function showModel(string $name): Response
4378
return $response;
4479
}
4580

46-
public function generate(string $model, string $prompt): Response
81+
public function generate(string $model, string $prompt, bool $stream = false): Response|StreamedResponse
4782
{
48-
$response = Http::timeout(60)->post($this->clientPath.'/api/generate', [
49-
'model' => $model,
50-
'prompt' => $prompt,
51-
'stream' => false
52-
]);
83+
if(!$stream)
84+
{
85+
86+
$response = Http::timeout(60)->post($this->clientPath.'/api/generate', [
87+
'model' => $model,
88+
'prompt' => $prompt,
89+
'stream' => false
90+
]);
91+
92+
return $response;
93+
}
5394

54-
return $response;
95+
96+
return $this->streamCall(fn() => Http::withOptions(['stream' => true])
97+
->post($this->clientPath.'/api/generate', [
98+
'model' => $model,
99+
'prompt' => $prompt,
100+
])
101+
->body()
102+
);
55103
}
56104

57105
public function tags(): Response
@@ -67,4 +115,25 @@ public function models(): Response
67115

68116
return $response;
69117
}
118+
119+
private function streamCall($streamCall)
120+
{
121+
$response = new StreamedResponse(function () use ($streamCall) {
122+
$stream = $streamCall();
123+
// Read the response line by line
124+
foreach (explode("\n", $stream) as $line) {
125+
if (!empty($line)) {
126+
echo "data: " . $line . "\n\n";
127+
ob_flush();
128+
flush();
129+
}
130+
}
131+
});
132+
133+
$response->headers->set('Content-Type', 'text/event-stream');
134+
$response->headers->set('Cache-Control', 'no-cache');
135+
$response->headers->set('Connection', 'keep-alive');
136+
137+
return $response;
138+
}
70139
}

config/ai.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
return [
44

5-
'text_client_path' => env('AI_TEXT_CLIENT_PATH', 'http://ollama:11434')
5+
'text_client_path' => env('AI_TEXT_CLIENT_PATH', 'http://ollama:11434'),
66

7+
'ollama_allowed_models' => env('OLLAMA_ALLOWED_MODELS', 'mistral,distilgpt-2')
78
];

readme/docs.png

153 KB
Loading

routes/api.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
use Illuminate\Support\Facades\Route;
55

66
Route::prefix('v1')->name('api.v1.')->group(function () {
7+
Route::get('ollama/tags', [OllamaController::class, 'indexTags']);
78
Route::post('ollama/show-model', [OllamaController::class, 'showModel']);
89
Route::post('ollama/pull-model', [OllamaController::class, 'pullModel']);
10+
Route::post('ollama/create-model', [OllamaController::class, 'createModel']);
911
Route::delete('ollama/delete-model', [OllamaController::class, 'deleteModel']);
1012
Route::post('ollama/generate', [OllamaController::class, 'generateText']);
11-
Route::get('ollama/models', [OllamaController::class, 'showModels']);
13+
Route::get('ollama/models', [OllamaController::class, 'indexModels']);
1214
});

0 commit comments

Comments
 (0)