Skip to content

Commit 8a67534

Browse files
authored
ollama-client (#48)
1 parent d68151a commit 8a67534

File tree

5 files changed

+241
-0
lines changed

5 files changed

+241
-0
lines changed

python/ollama-client/README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Python Ollama-Client Function (HTTP)
2+
3+
Welcome to your Python Ollama Client Function. It uses the [ollama](https://github.com/ollama/ollama)
4+
library.
5+
6+
## The Function
7+
8+
Your Function can be found in `function/func.py`. It handles HTTP requests in
9+
the `handle(self,scope,receive,send)` which is also the ASGI's handle signature
10+
(It's ASGI compatible). The only requests handled elsewhere are readiness and
11+
liveness checks -- `ready` and `alive` functions respectivelly.
12+
13+
### What it does
14+
15+
During initialization, we set a the Ollama's client with the correct server
16+
adress. That's it. Everything else happens in the `handle` function itself.
17+
18+
`handle` function includes some error handling and simple http body extraction
19+
and subsequently it makes an API request to the ollama server using Ollama's
20+
`client.chat()` function.
21+
22+
### Expected data
23+
24+
Any `GET` request will simply echo the standard 'OK' string.
25+
26+
`POST` request should be in json format and include `prompt` key. This is your
27+
prompt for the LLM. Additionally you can include `model` key which is the name
28+
of the model you want to use.
29+
30+
Example of a curl command:
31+
32+
```bash
33+
# use the default model
34+
curl localhost:11434 -d '{"prompt":"How to cook eggs properly?"}'
35+
36+
# use different model
37+
curl localhost:11434 -d '{"prompt":"How to cook eggs properly?","model":"llama3.2:3b"}'
38+
```
39+
40+
These values are simply extracted from the request and if provided it feeds them
41+
to the request for the LLM in a ollama complient way (see the construction of
42+
`self.client.chat()` function call).
43+
44+
## Extra
45+
46+
As per usual, the Function also contains a readiness and liveness checks
47+
implemented at the bottom of the Function class in their matching function names.
48+
The `start` and `stop` function are also available. See the function comments
49+
for more descriptive information.
50+
51+
For more info about the Ollama library, please visit [ollama github page](https://github.com/ollama/ollama)
52+
53+
For more info about Functions, see [the complete documentation]('https://github.com/knative/func/tree/main/docs')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .func import new
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Function
2+
import logging
3+
from ollama import Client
4+
import json
5+
import os
6+
7+
def new():
8+
""" New is the only method that must be implemented by a Function.
9+
The instance returned can be of any name.
10+
"""
11+
return Function()
12+
13+
# helper function for sending responses
14+
async def send_it(send,msg:str|None):
15+
if msg == None:
16+
msg = ""
17+
18+
await send({
19+
'type': 'http.response.start',
20+
'status': 200,
21+
'headers': [
22+
[b'content-type', b'text/plain'],
23+
],
24+
})
25+
await send({
26+
'type': 'http.response.body',
27+
'body': msg.encode(),
28+
})
29+
30+
class Function:
31+
def __init__(self):
32+
""" The init method is an optional method where initialization can be
33+
performed. See the start method for a startup hook which includes
34+
configuration.
35+
"""
36+
self.client = Client(
37+
# where your OLLAMA server is running
38+
host=os.environ.get("OLLAMA_HOST","127.0.0.1:11434")
39+
)
40+
41+
async def handle(self, scope, receive, send):
42+
""" Handle all HTTP requests to this Function other than readiness
43+
and liveness probes.
44+
45+
To communicate with the LLM following curl data is expected:
46+
{
47+
"prompt":"Your prompt for LLM",
48+
"model": "Your preffered ollama-compatible model",
49+
}
50+
51+
Note: Both of these have defaults, therefore you dont need to
52+
provide them.
53+
54+
example: curl <host:port> -d '{"prompt":"What is philosophy exactly"}'
55+
"""
56+
logging.info("OK: Request Received")
57+
58+
if scope["method"] == "GET":
59+
await send_it(send,'OK')
60+
return
61+
62+
# 1) extract the whole body from request
63+
body = b''
64+
more_body = True
65+
while more_body:
66+
message = await receive()
67+
body += message.get('body', b'')
68+
more_body = message.get('more_body', False)
69+
70+
# 2) decode the request and fetch info
71+
data = json.loads(body.decode('utf-8'))
72+
prompt = data.get('prompt','Who are you?')
73+
model = data.get('model',"llama3.2:1b")
74+
75+
print(f"using model {model}")
76+
# 3) make /api/chat request to the ollama server
77+
response = self.client.chat(
78+
# assign your model here
79+
model=model,
80+
messages=[
81+
{
82+
'role':'user',
83+
'content':prompt,
84+
},
85+
])
86+
87+
# 4) return the response to the calling client
88+
await send_it(send,response.message.content)
89+
90+
def start(self, cfg):
91+
""" start is an optional method which is called when a new Function
92+
instance is started, such as when scaling up or during an update.
93+
Provided is a dictionary containing all environmental configuration.
94+
Args:
95+
cfg (Dict[str, str]): A dictionary containing environmental config.
96+
In most cases this will be a copy of os.environ, but it is
97+
best practice to use this cfg dict instead of os.environ.
98+
"""
99+
logging.info("Function starting")
100+
101+
def stop(self):
102+
""" stop is an optional method which is called when a function is
103+
stopped, such as when scaled down, updated, or manually canceled. Stop
104+
can block while performing function shutdown/cleanup operations. The
105+
process will eventually be killed if this method blocks beyond the
106+
platform's configured maximum studown timeout.
107+
"""
108+
logging.info("Function stopping")
109+
110+
def alive(self):
111+
""" alive is an optional method for performing a deep check on your
112+
Function's liveness. If removed, the system will assume the function
113+
is ready if the process is running. This is exposed by default at the
114+
path /health/liveness. The optional string return is a message.
115+
"""
116+
return True, "Alive"
117+
118+
def ready(self):
119+
""" ready is an optional method for performing a deep check on your
120+
Function's readiness. If removed, the system will assume the function
121+
is ready if the process is running. This is exposed by default at the
122+
path /health/rediness.
123+
"""
124+
return True, "Ready"
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[project]
2+
name = "function"
3+
description = ""
4+
version = "0.1.0"
5+
requires-python = ">=3.9"
6+
readme = "README.md"
7+
license = "MIT"
8+
dependencies = [
9+
"httpx",
10+
"pytest",
11+
"pytest-asyncio",
12+
"ollama"
13+
]
14+
authors = [
15+
{ name="Your Name", email="you@example.com"},
16+
]
17+
18+
[build-system]
19+
requires = ["hatchling"]
20+
build-backend = "hatchling.build"
21+
22+
[tool.pytest.ini_options]
23+
asyncio_mode = "strict"
24+
asyncio_default_fixture_loop_scope = "function"
25+
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""
2+
An example set of unit tests which confirm that the main handler (the
3+
callable function) returns 200 OK for a simple HTTP GET.
4+
"""
5+
import pytest
6+
from function import new
7+
8+
9+
@pytest.mark.asyncio
10+
async def test_function_handle():
11+
f = new() # Instantiate Function to Test
12+
13+
sent_ok = False
14+
sent_headers = False
15+
sent_body = False
16+
17+
# Mock Send
18+
async def send(message):
19+
nonlocal sent_ok
20+
nonlocal sent_headers
21+
nonlocal sent_body
22+
23+
if message.get('status') == 200:
24+
sent_ok = True
25+
26+
if message.get('type') == 'http.response.start':
27+
sent_headers = True
28+
29+
if message.get('type') == 'http.response.body':
30+
sent_body = True
31+
32+
# Invoke the Function
33+
await f.handle({}, {}, send)
34+
35+
# Assert send was called
36+
assert sent_ok, "Function did not send a 200 OK"
37+
assert sent_headers, "Function did not send headers"
38+
assert sent_body, "Function did not send a body"

0 commit comments

Comments
 (0)