Skip to content

Commit 746b877

Browse files
authored
feat(ollama): added streaming & tool call support for ollama, updated docs (#884)
1 parent be65bf7 commit 746b877

File tree

14 files changed

+868
-307
lines changed

14 files changed

+868
-307
lines changed

.github/CONTRIBUTING.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,14 @@ Access the application at [http://localhost:3000/](http://localhost:3000/)
164164

165165
To use local models with Sim:
166166

167-
1. Pull models using our helper script:
167+
1. Install Ollama and pull models:
168168

169169
```bash
170-
./apps/sim/scripts/ollama_docker.sh pull <model_name>
170+
# Install Ollama (if not already installed)
171+
curl -fsSL https://ollama.ai/install.sh | sh
172+
173+
# Pull a model (e.g., gemma3:4b)
174+
ollama pull gemma3:4b
171175
```
172176

173177
2. Start Sim with local model support:

README.md

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -59,27 +59,21 @@ docker compose -f docker-compose.prod.yml up -d
5959

6060
Access the application at [http://localhost:3000/](http://localhost:3000/)
6161

62-
#### Using Local Models
62+
#### Using Local Models with Ollama
6363

64-
To use local models with Sim:
65-
66-
1. Pull models using our helper script:
64+
Run Sim with local AI models using [Ollama](https://ollama.ai) - no external APIs required:
6765

6866
```bash
69-
./apps/sim/scripts/ollama_docker.sh pull <model_name>
70-
```
67+
# Start with GPU support (automatically downloads gemma3:4b model)
68+
docker compose -f docker-compose.ollama.yml --profile setup up -d
7169

72-
2. Start Sim with local model support:
70+
# For CPU-only systems:
71+
docker compose -f docker-compose.ollama.yml --profile cpu --profile setup up -d
72+
```
7373

74+
Wait for the model to download, then visit [http://localhost:3000](http://localhost:3000). Add more models with:
7475
```bash
75-
# With NVIDIA GPU support
76-
docker compose --profile local-gpu -f docker-compose.ollama.yml up -d
77-
78-
# Without GPU (CPU only)
79-
docker compose --profile local-cpu -f docker-compose.ollama.yml up -d
80-
81-
# If hosting on a server, update the environment variables in the docker-compose.prod.yml file to include the server's public IP then start again (OLLAMA_URL to i.e. http://1.1.1.1:11434)
82-
docker compose -f docker-compose.prod.yml up -d
76+
docker compose -f docker-compose.ollama.yml exec ollama ollama pull llama3.1:8b
8377
```
8478

8579
### Option 3: Dev Containers

apps/sim/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ ENCRYPTION_KEY=your_encryption_key # Use `openssl rand -hex 32` to generate
1515
# RESEND_API_KEY= # Uncomment and add your key from https://resend.com to send actual emails
1616
# If left commented out, emails will be logged to console instead
1717

18+
# Local AI Models (Optional)
19+
# OLLAMA_URL=http://localhost:11434 # URL for local Ollama server - uncomment if using local models
20+
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { type NextRequest, NextResponse } from 'next/server'
2+
import { env } from '@/lib/env'
3+
import { createLogger } from '@/lib/logs/console/logger'
4+
import type { ModelsObject } from '@/providers/ollama/types'
5+
6+
const logger = createLogger('OllamaModelsAPI')
7+
const OLLAMA_HOST = env.OLLAMA_URL || 'http://localhost:11434'
8+
9+
export const dynamic = 'force-dynamic'
10+
11+
/**
12+
* Get available Ollama models
13+
*/
14+
export async function GET(request: NextRequest) {
15+
try {
16+
logger.info('Fetching Ollama models', {
17+
host: OLLAMA_HOST,
18+
})
19+
20+
const response = await fetch(`${OLLAMA_HOST}/api/tags`, {
21+
headers: {
22+
'Content-Type': 'application/json',
23+
},
24+
})
25+
26+
if (!response.ok) {
27+
logger.warn('Ollama service is not available', {
28+
status: response.status,
29+
statusText: response.statusText,
30+
})
31+
return NextResponse.json({ models: [] })
32+
}
33+
34+
const data = (await response.json()) as ModelsObject
35+
const models = data.models.map((model) => model.name)
36+
37+
logger.info('Successfully fetched Ollama models', {
38+
count: models.length,
39+
models,
40+
})
41+
42+
return NextResponse.json({ models })
43+
} catch (error) {
44+
logger.error('Failed to fetch Ollama models', {
45+
error: error instanceof Error ? error.message : 'Unknown error',
46+
host: OLLAMA_HOST,
47+
})
48+
49+
// Return empty array instead of error to avoid breaking the UI
50+
return NextResponse.json({ models: [] })
51+
}
52+
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -405,33 +405,37 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
405405
// If there's no condition, the block should be shown
406406
if (!block.condition) return true
407407

408+
// If condition is a function, call it to get the actual condition object
409+
const actualCondition =
410+
typeof block.condition === 'function' ? block.condition() : block.condition
411+
408412
// Get the values of the fields this block depends on from the appropriate state
409-
const fieldValue = stateToUse[block.condition.field]?.value
410-
const andFieldValue = block.condition.and
411-
? stateToUse[block.condition.and.field]?.value
413+
const fieldValue = stateToUse[actualCondition.field]?.value
414+
const andFieldValue = actualCondition.and
415+
? stateToUse[actualCondition.and.field]?.value
412416
: undefined
413417

414418
// Check if the condition value is an array
415-
const isValueMatch = Array.isArray(block.condition.value)
419+
const isValueMatch = Array.isArray(actualCondition.value)
416420
? fieldValue != null &&
417-
(block.condition.not
418-
? !block.condition.value.includes(fieldValue as string | number | boolean)
419-
: block.condition.value.includes(fieldValue as string | number | boolean))
420-
: block.condition.not
421-
? fieldValue !== block.condition.value
422-
: fieldValue === block.condition.value
421+
(actualCondition.not
422+
? !actualCondition.value.includes(fieldValue as string | number | boolean)
423+
: actualCondition.value.includes(fieldValue as string | number | boolean))
424+
: actualCondition.not
425+
? fieldValue !== actualCondition.value
426+
: fieldValue === actualCondition.value
423427

424428
// Check both conditions if 'and' is present
425429
const isAndValueMatch =
426-
!block.condition.and ||
427-
(Array.isArray(block.condition.and.value)
430+
!actualCondition.and ||
431+
(Array.isArray(actualCondition.and.value)
428432
? andFieldValue != null &&
429-
(block.condition.and.not
430-
? !block.condition.and.value.includes(andFieldValue as string | number | boolean)
431-
: block.condition.and.value.includes(andFieldValue as string | number | boolean))
432-
: block.condition.and.not
433-
? andFieldValue !== block.condition.and.value
434-
: andFieldValue === block.condition.and.value)
433+
(actualCondition.and.not
434+
? !actualCondition.and.value.includes(andFieldValue as string | number | boolean)
435+
: actualCondition.and.value.includes(andFieldValue as string | number | boolean))
436+
: actualCondition.and.not
437+
? andFieldValue !== actualCondition.and.value
438+
: andFieldValue === actualCondition.and.value)
435439

436440
return isValueMatch && isAndValueMatch
437441
})

apps/sim/blocks/blocks/agent.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ import {
1212
MODELS_WITH_TEMPERATURE_SUPPORT,
1313
providers,
1414
} from '@/providers/utils'
15+
16+
// Get current Ollama models dynamically
17+
const getCurrentOllamaModels = () => {
18+
return useOllamaStore.getState().models
19+
}
20+
1521
import { useOllamaStore } from '@/stores/ollama/store'
1622
import type { ToolResponse } from '@/tools/types'
1723

@@ -213,14 +219,18 @@ Create a system prompt appropriately detailed for the request, using clear langu
213219
password: true,
214220
connectionDroppable: false,
215221
required: true,
216-
// Hide API key for all hosted models when running on hosted version
222+
// Hide API key for hosted models and Ollama models
217223
condition: isHosted
218224
? {
219225
field: 'model',
220226
value: getHostedModels(),
221227
not: true, // Show for all models EXCEPT those listed
222228
}
223-
: undefined, // Show for all models in non-hosted environments
229+
: () => ({
230+
field: 'model',
231+
value: getCurrentOllamaModels(),
232+
not: true, // Show for all models EXCEPT Ollama models
233+
}),
224234
},
225235
{
226236
id: 'azureEndpoint',

apps/sim/blocks/types.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,16 +118,27 @@ export interface SubBlockConfig {
118118
hidden?: boolean
119119
description?: string
120120
value?: (params: Record<string, any>) => string
121-
condition?: {
122-
field: string
123-
value: string | number | boolean | Array<string | number | boolean>
124-
not?: boolean
125-
and?: {
126-
field: string
127-
value: string | number | boolean | Array<string | number | boolean> | undefined
128-
not?: boolean
129-
}
130-
}
121+
condition?:
122+
| {
123+
field: string
124+
value: string | number | boolean | Array<string | number | boolean>
125+
not?: boolean
126+
and?: {
127+
field: string
128+
value: string | number | boolean | Array<string | number | boolean> | undefined
129+
not?: boolean
130+
}
131+
}
132+
| (() => {
133+
field: string
134+
value: string | number | boolean | Array<string | number | boolean>
135+
not?: boolean
136+
and?: {
137+
field: string
138+
value: string | number | boolean | Array<string | number | boolean> | undefined
139+
not?: boolean
140+
}
141+
})
131142
// Props specific to 'code' sub-block type
132143
language?: 'javascript' | 'json'
133144
generationType?: GenerationType

apps/sim/executor/resolver/resolver.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export class InputResolver {
5858

5959
/**
6060
* Evaluates if a sub-block should be active based on its condition
61-
* @param condition - The condition to evaluate
61+
* @param condition - The condition to evaluate (can be static object or function)
6262
* @param currentValues - Current values of all inputs
6363
* @returns True if the sub-block should be active
6464
*/
@@ -70,37 +70,46 @@ export class InputResolver {
7070
not?: boolean
7171
and?: { field: string; value: any; not?: boolean }
7272
}
73+
| (() => {
74+
field: string
75+
value: any
76+
not?: boolean
77+
and?: { field: string; value: any; not?: boolean }
78+
})
7379
| undefined,
7480
currentValues: Record<string, any>
7581
): boolean {
7682
if (!condition) return true
7783

84+
// If condition is a function, call it to get the actual condition object
85+
const actualCondition = typeof condition === 'function' ? condition() : condition
86+
7887
// Get the field value
79-
const fieldValue = currentValues[condition.field]
88+
const fieldValue = currentValues[actualCondition.field]
8089

8190
// Check if the condition value is an array
82-
const isValueMatch = Array.isArray(condition.value)
91+
const isValueMatch = Array.isArray(actualCondition.value)
8392
? fieldValue != null &&
84-
(condition.not
85-
? !condition.value.includes(fieldValue)
86-
: condition.value.includes(fieldValue))
87-
: condition.not
88-
? fieldValue !== condition.value
89-
: fieldValue === condition.value
93+
(actualCondition.not
94+
? !actualCondition.value.includes(fieldValue)
95+
: actualCondition.value.includes(fieldValue))
96+
: actualCondition.not
97+
? fieldValue !== actualCondition.value
98+
: fieldValue === actualCondition.value
9099

91100
// Check both conditions if 'and' is present
92101
const isAndValueMatch =
93-
!condition.and ||
102+
!actualCondition.and ||
94103
(() => {
95-
const andFieldValue = currentValues[condition.and!.field]
96-
return Array.isArray(condition.and!.value)
104+
const andFieldValue = currentValues[actualCondition.and!.field]
105+
return Array.isArray(actualCondition.and!.value)
97106
? andFieldValue != null &&
98-
(condition.and!.not
99-
? !condition.and!.value.includes(andFieldValue)
100-
: condition.and!.value.includes(andFieldValue))
101-
: condition.and!.not
102-
? andFieldValue !== condition.and!.value
103-
: andFieldValue === condition.and!.value
107+
(actualCondition.and!.not
108+
? !actualCondition.and!.value.includes(andFieldValue)
109+
: actualCondition.and!.value.includes(andFieldValue))
110+
: actualCondition.and!.not
111+
? andFieldValue !== actualCondition.and!.value
112+
: andFieldValue === actualCondition.and!.value
104113
})()
105114

106115
return isValueMatch && isAndValueMatch

0 commit comments

Comments
 (0)