Skip to content

Commit

Permalink
Merge pull request #11 from mutablelogic/v1
Browse files Browse the repository at this point in the history
Added streaming outputs to client
  • Loading branch information
djthorpe authored May 9, 2024
2 parents 61f10a1 + 4111a59 commit 3a721be
Show file tree
Hide file tree
Showing 29 changed files with 800 additions and 112 deletions.
35 changes: 22 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,20 @@ This repository contains a generic HTTP client which can be adapted to provide:
* Ability to send files and data of type `multipart/form-data`
* Ability to send data of type `application/x-www-form-urlencoded`
* Debugging capabilities to see the request and response data
* Streaming JSON responses

Documentation: https://pkg.go.dev/github.com/mutablelogic/go-client/pkg/client

There are also some example API clients:
There are also some example clients which use this library:

* [Bitwarden Client](https://github.com/mutablelogic/go-client/tree/main/pkg/bitwarden)
* [Elevenlabs Client](https://github.com/mutablelogic/go-client/tree/main/pkg/elevenlabs)
* [Home Assistant Client](https://github.com/mutablelogic/go-client/tree/main/pkg/homeassistant)
* [Bitwarden API Client](https://github.com/mutablelogic/go-client/tree/main/pkg/bitwarden)
* [Elevenlabs API Client](https://github.com/mutablelogic/go-client/tree/main/pkg/elevenlabs)
* [Home Assistant API Client](https://github.com/mutablelogic/go-client/tree/main/pkg/homeassistant)
* [IPify Client](https://github.com/mutablelogic/go-client/tree/main/pkg/ipify)
* [Mistral API Client](https://github.com/mutablelogic/go-client/tree/main/pkg/mistral)
* [NewsAPI client](https://github.com/mutablelogic/go-client/tree/main/pkg/newsapi)
* [OpenAI client](https://github.com/mutablelogic/go-client/tree/main/pkg/openai)
* [Ollama API client](https://github.com/mutablelogic/go-client/tree/main/pkg/ollama)
* [OpenAI API client](https://github.com/mutablelogic/go-client/tree/main/pkg/openai)

## Basic Usage

Expand All @@ -29,7 +31,7 @@ to a JSON endpoint:
package main

import (
client "github.com/mutablelogic/go-client"
client "github.com/mutablelogic/go-client/pkg/client"
)

func main() {
Expand Down Expand Up @@ -69,9 +71,8 @@ Various options can be passed to the client `New` method to control its behaviou
The first argument to the `Do` method is the payload to send to the server, when set. You can create a payload
using the following methods:

* `client.NewRequest(accept string)` returns a new empty payload which defaults to GET. The accept parameter is the
accepted mime-type of the response.
* `client.NewJSONRequest(payload any, accept string)` returns a new request with a JSON payload which defaults to GET.
* `client.NewRequest()` returns a new empty payload which defaults to GET.
* `client.NewJSONRequest(payload any, accept string)` returns a new request with a JSON payload which defaults to POST.
* `client.NewMultipartRequest(payload any, accept string)` returns a new request with a Multipart Form data payload which
defaults to POST.
* `client.NewFormRequest(payload any, accept string)` returns a new request with a Form data payload which defaults to POST.
Expand All @@ -82,7 +83,7 @@ For example,
package main

import (
client "github.com/mutablelogic/go-client"
client "github.com/mutablelogic/go-client/pkg/client"
)

func main() {
Expand All @@ -97,7 +98,7 @@ func main() {
Reply string `json:"reply"`
}
request.Prompt = "Hello, world!"
payload := client.NewJSONRequest(request, "application/json")
payload := client.NewJSONRequest(request)
if err := c.Do(payload, &response, OptPath("test")); err != nil {
// Handle error
}
Expand Down Expand Up @@ -145,7 +146,9 @@ Various options can be passed to modify each individual request when using the `
* `OptToken(value Token)` adds an authorization header (overrides the client OptReqToken option)
* `OptQuery(value url.Values)` sets the query parameters to a request
* `OptHeader(key, value string)` appends a custom header to the request

* `OptResponse(func() error)` allows you to set a callback function to process a streaming response.
See below for more details.
* `OptNoTimeout()` disables the timeout on the request, which is useful for long running requests

## Authentication

Expand All @@ -155,7 +158,7 @@ The authentication token can be set as follows:
package main

import (
client "github.com/mutablelogic/go-client"
client "github.com/mutablelogic/go-client/pkg/client"
)

func main() {
Expand All @@ -182,3 +185,9 @@ You can create a payload with form data:
* `client.NewMultipartRequest(payload any, accept string)` returns a new request with a Multipart Form data payload which defaults to POST. This is useful for file uploads.

The payload should be a `struct` where the fields are converted to form tuples. File uploads require a field of type `multipart.File`.

## Streaming Responses

If the returned content is a stream of JSON responses, then you can use the `OptResponse(fn func() error)` option, which
will be called by the `Do` method for each response. The function should return an error if the stream should be terminated.
Usually, you would pair this option with `OptNoTimeout` to prevent the request from timing out.
8 changes: 7 additions & 1 deletion cmd/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

func main() {
name := path.Base(os.Args[0])
flags, err := NewFlags(name, os.Args[1:], OpenAIFlags, MistralFlags, ElevenlabsFlags, HomeAssistantFlags, NewsAPIFlags)
flags, err := NewFlags(name, os.Args[1:], OpenAIFlags, MistralFlags, ElevenlabsFlags, HomeAssistantFlags, NewsAPIFlags, OllamaFlags)
if err != nil {
if err != flag.ErrHelp {
fmt.Fprintln(os.Stderr, err)
Expand Down Expand Up @@ -71,6 +71,12 @@ func main() {
os.Exit(1)
}

cmd, err = OllamaRegister(cmd, opts, flags)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}

// Run command
if err := Run(cmd, flags); err != nil {
if errors.Is(err, flag.ErrHelp) {
Expand Down
45 changes: 45 additions & 0 deletions cmd/cli/ollama.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package main

import (
// Package imports
"github.com/mutablelogic/go-client/pkg/client"
"github.com/mutablelogic/go-client/pkg/ollama"
)

/////////////////////////////////////////////////////////////////////
// REGISTER FUNCTIONS

func OllamaFlags(flags *Flags) {
flags.String("ollama-endpoint", "${OLLAMA_ENDPOINT}", "Ollama endpoint url")
}

func OllamaRegister(cmd []Client, opts []client.ClientOpt, flags *Flags) ([]Client, error) {
ollama, err := ollama.New(flags.GetString("ollama-endpoint"), opts...)
if err != nil {
return nil, err
}

// Register commands
cmd = append(cmd, Client{
ns: "ollama",
cmd: []Command{
{Name: "models", Description: "List local models", MinArgs: 2, MaxArgs: 2, Fn: ollamaListModels(ollama, flags)},
},
})

// Return success
return cmd, nil
}

/////////////////////////////////////////////////////////////////////
// API CALL FUNCTIONS

func ollamaListModels(client *ollama.Client, flags *Flags) CommandFn {
return func() error {
if models, err := client.ListModels(); err != nil {
return err
} else {
return flags.Write(models)
}
}
}
Binary file added etc/test/IMG_20130413_095348.JPG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes.
50 changes: 41 additions & 9 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
"mime"
Expand Down Expand Up @@ -43,7 +44,6 @@ type Client struct {
}

type ClientOpt func(*Client) error
type RequestOpt func(*http.Request) error

///////////////////////////////////////////////////////////////////////////////
// GLOBALS
Expand All @@ -54,6 +54,7 @@ const (
PathSeparator = string(os.PathSeparator)
ContentTypeAny = "*/*"
ContentTypeJson = "application/json"
ContentTypeJsonStream = "application/x-ndjson"
ContentTypeTextXml = "text/xml"
ContentTypeApplicationXml = "application/xml"
ContentTypeTextPlain = "text/plain"
Expand Down Expand Up @@ -124,12 +125,12 @@ func (client *Client) DoWithContext(ctx context.Context, in Payload, out any, op
now := time.Now()
if !client.ts.IsZero() && client.rate > 0.0 {
next := client.ts.Add(time.Duration(float32(time.Second) / client.rate))
if next.After(now) {
if next.After(now) { // TODO allow ctx to cancel the sleep
time.Sleep(next.Sub(now))
}
}

// Set timestamp at return
// Set timestamp at return, for rate limiting
defer func(now time.Time) {
client.ts = now
}(now)
Expand Down Expand Up @@ -164,7 +165,7 @@ func (client *Client) Request(req *http.Request, out any, opts ...RequestOpt) er
now := time.Now()
if !client.ts.IsZero() && client.rate > 0.0 {
next := client.ts.Add(time.Duration(float32(time.Second) / client.rate))
if next.After(now) {
if next.After(now) { // TODO allow ctx to cancel the sleep
time.Sleep(next.Sub(now))
}
}
Expand Down Expand Up @@ -235,12 +236,23 @@ func (client *Client) request(ctx context.Context, method, accept, mimetype stri
// Do will make a JSON request, populate an object with the response and return any errors
func do(client *http.Client, req *http.Request, accept string, strict bool, out any, opts ...RequestOpt) error {
// Apply request options
reqopts := requestOpts{
Request: req,
}
for _, opt := range opts {
if err := opt(req); err != nil {
if err := opt(&reqopts); err != nil {
return err
}
}

// NoTimeout
if reqopts.noTimeout {
defer func(v time.Duration) {
client.Timeout = v
}(client.Timeout)
client.Timeout = 0
}

// Do the request
response, err := client.Do(req)
if err != nil {
Expand Down Expand Up @@ -276,16 +288,31 @@ func do(client *http.Client, req *http.Request, accept string, strict bool, out
return nil
}

// Decode the body
// Decode the body - and call any callback once the body has been decoded
switch mimetype {
case ContentTypeJson:
if err := json.NewDecoder(response.Body).Decode(out); err != nil {
return err
case ContentTypeJson, ContentTypeJsonStream:
dec := json.NewDecoder(response.Body)
for {
if err := dec.Decode(out); errors.Is(err, io.EOF) {
break
} else if err != nil {
return err
}
if reqopts.callback != nil {
if err := reqopts.callback(); err != nil {
return err
}
}
}
case ContentTypeTextXml, ContentTypeApplicationXml:
if err := xml.NewDecoder(response.Body).Decode(out); err != nil {
return err
}
if reqopts.callback != nil {
if err := reqopts.callback(); err != nil {
return err
}
}
default:
if v, ok := out.(Unmarshaler); ok {
return v.Unmarshal(mimetype, response.Body)
Expand All @@ -296,6 +323,11 @@ func do(client *http.Client, req *http.Request, accept string, strict bool, out
} else {
return ErrInternalAppError.Withf("do: response does not implement Unmarshaler for %q", mimetype)
}
if reqopts.callback != nil {
if err := reqopts.callback(); err != nil {
return err
}
}
}

// Return success
Expand Down
26 changes: 24 additions & 2 deletions pkg/client/doc.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
/*
client impleemts a generic REST API client which can be used for creating
gateway-specific clients
Implements a generic REST API client which can be used for creating
gateway-specific clients. Basic usage:
package main
import (
client "github.com/mutablelogic/go-client/pkg/client"
)
func main() {
// Create a new client
c := client.New(client.OptEndpoint("https://api.example.com/api/v1"))
// Send a GET request, populating a struct with the response
var response struct {
Message string `json:"message"`
}
if err := c.Do(nil, &response, OptPath("test")); err != nil {
// Handle error
}
// Print the response
fmt.Println(response.Message)
}
*/
package client
Loading

0 comments on commit 3a721be

Please sign in to comment.