Skip to content

Commit ff18589

Browse files
Merge pull request #55 from dreadnode/ads/eng-1506-bug-dyana-bug-python-version-issue-in-ollama-loader54
chore: refactor ollama loader to fix pydantic errors
2 parents 221b4ed + 0b9a583 commit ff18589

4 files changed

Lines changed: 157 additions & 41 deletions

File tree

dyana/loaders/ollama/Dockerfile

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,20 @@ FROM ollama/ollama
22

33
WORKDIR /app
44

5-
RUN apt-get update && apt-get install -y build-essential python3-pip python3
5+
RUN apt-get update && apt-get install -y --no-install-recommends gnupg build-essential curl
6+
RUN echo "deb http://ppa.launchpad.net/deadsnakes/ppa/ubuntu focal main" > /etc/apt/sources.list.d/deadsnakes.list
7+
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys f23c5a6cf475977595c89f51ba6932366a755776
8+
RUN apt-get update && DEBIAN_FRONTEND=noninteractive TZ=America/New_York apt-get install -y --no-install-recommends python3.12
9+
10+
RUN curl https://bootstrap.pypa.io/get-pip.py -o pip.py
11+
RUN python3.12 pip.py
12+
613
COPY dyana.py .
714
COPY dyana-requirements-gpu.txt .
815
RUN pip install --no-cache-dir -r dyana-requirements-gpu.txt
916

1017
COPY requirements.txt .
11-
COPY main.py .
12-
1318
RUN pip install --no-cache-dir -r requirements.txt
19+
COPY dyana.py main.py ./
1420

15-
ENTRYPOINT ["python3", "-W", "ignore", "main.py"]
21+
ENTRYPOINT ["python3.12", "-W", "ignore", "main.py"]

dyana/loaders/ollama/main.py

Lines changed: 133 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,144 @@
11
import argparse
2+
import json
23
import os
4+
import sys
35
import time
6+
import traceback
7+
from typing import Any
48

59
from ollama import Client
610

7-
from dyana import Profiler # type: ignore[attr-defined]
811

9-
if __name__ == "__main__":
10-
parser = argparse.ArgumentParser(description="Run an Ollama model")
11-
parser.add_argument("--model", help="Name of the Ollama model to profile", required=True)
12-
parser.add_argument("--input", help="The input sentence", default="This is an example sentence.")
13-
args = parser.parse_args()
12+
def ensure_output(data: str) -> None:
13+
"""Write to both stdout and stderr to ensure output is captured"""
14+
print(data)
15+
print(data, file=sys.stderr)
16+
sys.stdout.flush()
17+
sys.stderr.flush()
1418

15-
# start ollama server
16-
os.system("ollama serve > /dev/null 2>&1 &")
17-
for _ in range(30):
18-
# print(f"waiting for ollama to start... {i}")
19-
if os.system("ollama ls > /dev/null 2>&1") == 0:
20-
break
21-
time.sleep(1)
2219

23-
# create profiler after the server is started
24-
profiler: Profiler = Profiler(gpu=True)
20+
try:
21+
ensure_output(json.dumps({"status": "script_started"}))
2522

2623
try:
27-
client = Client(
28-
host="http://127.0.0.1:11434",
29-
)
30-
response = client.chat(
31-
model=args.model,
32-
messages=[
33-
{
34-
"role": "user",
35-
"content": args.input,
36-
},
37-
],
38-
)
39-
40-
profiler.on_stage("after_inference")
41-
42-
print(response)
43-
44-
except Exception as e:
45-
profiler.track_error("ollama", str(e))
24+
from dyana import Profiler # type: ignore[attr-defined]
25+
except ImportError:
26+
# Only define our own Profiler if the import fails
27+
class Profiler: # type: ignore
28+
def __init__(self, gpu: bool = False) -> None:
29+
self.gpu = gpu
30+
31+
def on_stage(self, stage: str) -> None:
32+
pass
33+
34+
def track_error(self, source: str, error: str) -> None:
35+
pass
36+
37+
if __name__ == "__main__":
38+
parser = argparse.ArgumentParser(description="Run an Ollama model")
39+
parser.add_argument("--model", help="Name of the Ollama model to profile", required=True)
40+
parser.add_argument("--input", help="The input sentence", default="This is an example sentence.")
41+
args = parser.parse_args()
42+
43+
result: dict[str, Any] = {
44+
"status": "started",
45+
"model": args.model,
46+
"input": args.input,
47+
"timestamp": time.time(),
48+
}
49+
ensure_output(json.dumps(result))
50+
51+
try:
52+
# Create profiler
53+
profiler = Profiler(gpu=True)
54+
55+
os.makedirs("/root/.ollama/manifests", exist_ok=True)
56+
os.makedirs("/root/.ollama/cache", exist_ok=True)
57+
58+
try:
59+
os.chmod("/root/.ollama", 0o755)
60+
os.chmod("/root/.ollama/models", 0o755)
61+
os.chmod("/root/.ollama/manifests", 0o755)
62+
os.chmod("/root/.ollama/cache", 0o755)
63+
except Exception as perm_error:
64+
result["permission_warning"] = str(perm_error)
65+
66+
# Start ollama server
67+
os.system("ollama serve > /dev/null 2>&1 &")
68+
69+
# Wait for server to start
70+
server_started = False
71+
for i in range(30):
72+
if os.system("ollama ls > /dev/null 2>&1") == 0:
73+
server_started = True
74+
result["startup_time"] = i
75+
break
76+
time.sleep(1)
77+
78+
if not server_started:
79+
result["status"] = "error"
80+
result["error"] = "Failed to start Ollama server after 30 seconds"
81+
ensure_output(json.dumps(result))
82+
sys.exit(1)
83+
84+
# Record initialization stage
85+
profiler.on_stage("initialization")
86+
87+
# Connect to the ollama server
88+
client = Client(host="http://127.0.0.1:11434")
89+
90+
# Check if model exists locally without trying to pull it first
91+
models = client.list()
92+
model_exists = any(m.get("name", "") == args.model for m in models.get("models", []))
93+
94+
result["model_found"] = model_exists
95+
96+
if model_exists:
97+
# Skip pulling if the model already exists
98+
result["status"] = "running_inference"
99+
ensure_output(json.dumps(result))
100+
101+
# Run inference with existing model
102+
chat_response = client.chat(
103+
model=args.model,
104+
messages=[{"role": "user", "content": args.input}],
105+
)
106+
107+
# Mark completion of inference
108+
profiler.on_stage("after_inference")
109+
110+
# Update result with success
111+
result["status"] = "success"
112+
if hasattr(chat_response, "model_dump"):
113+
result["response"] = chat_response.model_dump()
114+
else:
115+
result["response"] = str(chat_response)
116+
else:
117+
# Can't pull models due to read-only filesystem
118+
result["status"] = "error"
119+
result["error"] = (
120+
"Model not found locally and cannot pull due to read-only filesystem. Please pull the model on your host with 'ollama pull "
121+
+ args.model
122+
+ "' before running dyana."
123+
)
124+
ensure_output(json.dumps(result))
125+
126+
except Exception as e:
127+
# Handle any exceptions
128+
result["status"] = "error"
129+
result["error"] = str(e)
130+
result["traceback"] = traceback.format_exc()
131+
if "profiler" in locals():
132+
profiler.track_error("ollama", str(e))
133+
134+
# Output final result
135+
ensure_output(json.dumps(result, default=str))
136+
137+
except Exception as outer_e:
138+
# Last resort error handling
139+
emergency_data: dict[str, str] = {
140+
"status": "fatal_error",
141+
"error": str(outer_e),
142+
"traceback": traceback.format_exc(),
143+
}
144+
ensure_output(json.dumps(emergency_data))
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
ollama==0.4.7
1+
ollama==0.4.7
2+
pydantic>=2.0.0

dyana/loaders/ollama/settings.yml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,22 @@ description: Loads and profiles models via an Ollama server. Local models on the
33
gpu: true
44

55
volumes:
6-
# on macOS
6+
# on macOS - mount with write permissions
77
- host: ~/.ollama/models
88
guest: /root/.ollama/models
9-
# on Linux
9+
options: "rw"
10+
# on Linux - mount with write permissions
1011
- host: /usr/share/ollama/.ollama/models
1112
guest: /root/.ollama/models
13+
options: "rw"
14+
# Add a writable directory for Ollama to store manifests
15+
- host: ~/.ollama/manifests
16+
guest: /root/.ollama/manifests
17+
options: "rw"
18+
# Add directory for model cache
19+
- host: ~/.ollama/cache
20+
guest: /root/.ollama/cache
21+
options: "rw"
1222

1323
args:
1424
- name: model

0 commit comments

Comments
 (0)