Skip to content

Commit a0ae35d

Browse files
Update Go SDK and examples
1 parent 8dca6e5 commit a0ae35d

File tree

14 files changed

+1337
-293
lines changed

14 files changed

+1337
-293
lines changed

.trunk/configs/cspell.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@
134134
"omitif",
135135
"omitnull",
136136
"openai",
137+
"openspeech",
137138
"operationreport",
138139
"PEMS",
139140
"pgconn",
@@ -175,6 +176,7 @@
175176
"textgeneration",
176177
"tidwall",
177178
"tinygo",
179+
"toolcalling",
178180
"tseslint",
179181
"tsrv",
180182
"typedarray",

sdk/go/examples/textgeneration/main.go

Lines changed: 7 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -6,154 +6,10 @@
66

77
package main
88

9-
import (
10-
"encoding/json"
11-
"fmt"
12-
"strings"
13-
14-
"github.com/hypermodeinc/modus/sdk/go/pkg/models"
15-
"github.com/hypermodeinc/modus/sdk/go/pkg/models/openai"
16-
)
17-
18-
// In this example, we will generate text using the OpenAI Chat model.
19-
// See https://platform.openai.com/docs/api-reference/chat/create for more details
20-
// about the options available on the model, which you can set on the input object.
21-
22-
// This model name should match the one defined in the modus.json manifest file.
23-
const modelName = "text-generator"
24-
25-
// This function generates some text based on the instruction and prompt provided.
26-
func GenerateText(instruction, prompt string) (string, error) {
27-
28-
// The imported ChatModel type follows the OpenAI Chat completion model input format.
29-
model, err := models.GetModel[openai.ChatModel](modelName)
30-
if err != nil {
31-
return "", err
32-
}
33-
34-
// We'll start by creating an input object using the instruction and prompt provided.
35-
input, err := model.CreateInput(
36-
openai.NewSystemMessage(instruction),
37-
openai.NewUserMessage(prompt),
38-
// ... if we wanted to add more messages, we could do so here.
39-
)
40-
if err != nil {
41-
return "", err
42-
}
43-
44-
// This is one of many optional parameters available for the OpenAI Chat model.
45-
input.Temperature = 0.7
46-
47-
// Here we invoke the model with the input we created.
48-
output, err := model.Invoke(input)
49-
if err != nil {
50-
return "", err
51-
}
52-
53-
// The output is also specific to the ChatModel interface.
54-
// Here we return the trimmed content of the first choice.
55-
return strings.TrimSpace(output.Choices[0].Message.Content), nil
56-
}
57-
58-
// This function generates a single product.
59-
func GenerateProduct(category string) (*Product, error) {
60-
61-
// We can get creative with the instruction and prompt to guide the model
62-
// in generating the desired output. Here we provide a sample JSON of the
63-
// object we want the model to generate.
64-
instruction := "Generate a product for the category provided.\n" +
65-
"Only respond with valid JSON object in this format:\n" + sampleProductJson
66-
prompt := fmt.Sprintf(`The category is "%s".`, category)
67-
68-
// Set up the input for the model, creating messages for the instruction and prompt.
69-
model, err := models.GetModel[openai.ChatModel](modelName)
70-
if err != nil {
71-
return nil, err
72-
}
73-
input, err := model.CreateInput(
74-
openai.NewSystemMessage(instruction),
75-
openai.NewUserMessage(prompt),
76-
)
77-
if err != nil {
78-
return nil, err
79-
}
80-
81-
// Let's increase the temperature to get more creative responses.
82-
// Be careful though, if the temperature is too high, the model may generate invalid JSON.
83-
input.Temperature = 1.2
84-
85-
// This model also has a response format parameter that can be set to JSON,
86-
// Which, along with the instruction, can help guide the model in generating valid JSON output.
87-
input.ResponseFormat = openai.ResponseFormatJson
88-
89-
// Here we invoke the model with the input we created.
90-
output, err := model.Invoke(input)
91-
if err != nil {
92-
return nil, err
93-
}
94-
95-
// The output should contain the JSON string we asked for.
96-
content := strings.TrimSpace(output.Choices[0].Message.Content)
97-
98-
// We can now parse the JSON string as a Product object.
99-
var product Product
100-
if err := json.Unmarshal([]byte(content), &product); err != nil {
101-
return nil, fmt.Errorf("failed to parse JSON: %w", err)
102-
}
103-
104-
return &product, nil
105-
}
106-
107-
// This function generates multiple product.
108-
func GenerateProducts(category string, quantity int) ([]Product, error) {
109-
110-
// Similar to the previous example above, we can tailor the instruction and prompt
111-
// to guide the model in generating the desired output. Note that understanding the behavior
112-
// of the model is important to get the desired results. In this case, we need the model
113-
// to return an _object_ containing an array, not an array of objects directly.
114-
// That's because the model will not reliably generate an array of objects directly.
115-
instruction := fmt.Sprintf("Generate %d products for the category provided.\n"+
116-
"Only respond with a valid JSON object containing a valid JSON array named 'list', in this format:\n"+
117-
`{"list":[%s]}`, quantity, sampleProductJson)
118-
prompt := fmt.Sprintf(`The category is "%s".`, category)
119-
120-
// Set up the input for the model, creating messages for the instruction and prompt.
121-
model, err := models.GetModel[openai.ChatModel](modelName)
122-
if err != nil {
123-
return nil, err
124-
}
125-
input, err := model.CreateInput(
126-
openai.NewSystemMessage(instruction),
127-
openai.NewUserMessage(prompt),
128-
)
129-
if err != nil {
130-
return nil, err
131-
}
132-
133-
// Adjust the model inputs, just like in the previous example.
134-
// Be careful, if the temperature is too high, the model may generate invalid JSON.
135-
input.Temperature = 1.2
136-
input.ResponseFormat = openai.ResponseFormatJson
137-
138-
// Here we invoke the model with the input we created.
139-
output, err := model.Invoke(input)
140-
if err != nil {
141-
return nil, err
142-
}
143-
144-
// The output should contain the JSON string we asked for.
145-
content := strings.TrimSpace(output.Choices[0].Message.Content)
146-
147-
// We can parse that JSON to a compatible object, to get the data we're looking for.
148-
var data map[string][]Product
149-
if err := json.Unmarshal([]byte(content), &data); err != nil {
150-
return nil, fmt.Errorf("failed to parse JSON: %w", err)
151-
}
152-
153-
// Now we can extract the list of products from the data.
154-
products, found := data["list"]
155-
if !found {
156-
return nil, fmt.Errorf("expected 'list' key in JSON object")
157-
}
158-
return products, nil
159-
}
9+
// The examples have been split into separate files for clarity.
10+
// See each of the following files for more details about the specific example.
11+
//
12+
// - simple.go
13+
// - products.go
14+
// - media.go
15+
// - toolcalling.go
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/*
2+
* This example is part of the Modus project, licensed under the Apache License 2.0.
3+
* You may modify and use this example in accordance with the license.
4+
* See the LICENSE file that accompanied this code for further details.
5+
*/
6+
7+
package main
8+
9+
import (
10+
"fmt"
11+
"math/rand/v2"
12+
"strings"
13+
14+
"github.com/hypermodeinc/modus/sdk/go/pkg/http"
15+
"github.com/hypermodeinc/modus/sdk/go/pkg/models"
16+
"github.com/hypermodeinc/modus/sdk/go/pkg/models/openai"
17+
)
18+
19+
// These examples demonstrate how to use audio or image data with OpenAI chat models.
20+
// Currently, audio can be used for input or output, but images can be used only for input.
21+
22+
// This type is used in these examples to represent images or audio.
23+
type Media struct {
24+
25+
// The content type of the media.
26+
ContentType string
27+
28+
// The binary data of the media.
29+
// This value will be base64 encoded when used in an API response.
30+
Data []byte
31+
32+
// A text description or transcription of the media.
33+
Text string
34+
}
35+
36+
// This function generates an audio response based on the instruction and prompt provided.
37+
func GenerateAudio(instruction, prompt string) (*Media, error) {
38+
39+
// Note, this is similar to GenerateText above, but with audio output requested.
40+
41+
// We'll generate the audio using an audio-enabled OpenAI chat model.
42+
model, err := models.GetModel[openai.ChatModel]("audio-model")
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
input, err := model.CreateInput(
48+
openai.NewSystemMessage(instruction),
49+
openai.NewUserMessage(prompt),
50+
)
51+
if err != nil {
52+
return nil, err
53+
}
54+
55+
input.Temperature = 0.7
56+
57+
// Request audio output from the model.
58+
// Note, this is a convenience method that requests audio modality and sets the voice and format.
59+
// You can also set these values manually on the input object, if you prefer.
60+
input.RequestAudioOutput("ash", "wav")
61+
62+
output, err := model.Invoke(input)
63+
if err != nil {
64+
return nil, err
65+
}
66+
67+
// Return the audio and its transcription.
68+
// Note that the message Content field will be empty for audio responses.
69+
// Instead, the text will be in the Message.Audio.Transcript field.
70+
audio := output.Choices[0].Message.Audio
71+
72+
media := &Media{
73+
ContentType: "audio/wav",
74+
Data: audio.Data,
75+
Text: strings.TrimSpace(audio.Transcript),
76+
}
77+
78+
return media, nil
79+
}
80+
81+
// This function generates text that describes the image at the provided url.
82+
// In this example the image url is passed to the model, and the model retrieves the image.
83+
func DescribeImage(url string) (string, error) {
84+
85+
// Note that because the model retrieves the image, any URL can be used.
86+
// However, this means that there is a risk of sending data to an unauthorized host, if the URL is not hardcoded or sanitized.
87+
// See the DescribeRandomImage function below for a safer approach.
88+
89+
model, err := models.GetModel[openai.ChatModel]("text-generator")
90+
if err != nil {
91+
return "", err
92+
}
93+
94+
input, err := model.CreateInput(
95+
openai.NewUserMessageFromParts(
96+
openai.NewTextContentPart("Describe this image."),
97+
openai.NewImageContentPartFromUrl(url),
98+
),
99+
)
100+
if err != nil {
101+
return "", err
102+
}
103+
104+
output, err := model.Invoke(input)
105+
if err != nil {
106+
return "", err
107+
}
108+
109+
return strings.TrimSpace(output.Choices[0].Message.Content), nil
110+
}
111+
112+
// This function fetches a random image, and then generates text that describes it.
113+
// In this example the image is retrieved by the function before passing it as data to the model.
114+
func DescribeRandomImage() (*Media, error) {
115+
116+
// Because this approach fetches the image directly, it is safer than the DescribeImage function above.
117+
// The host URL is allow-listed in the modus.json file, so we can trust the image source.
118+
119+
// Fetch a random image from the Picsum API. We'll just hardcode the size to make the demo simple to call.
120+
response, err := http.Fetch("https://picsum.photos/640/480")
121+
if err != nil {
122+
return nil, err
123+
}
124+
data := response.Body
125+
contentType := *response.Headers.Get("Content-Type")
126+
127+
// Describe the image using the OpenAI chat model.
128+
model, err := models.GetModel[openai.ChatModel]("text-generator")
129+
if err != nil {
130+
return nil, err
131+
}
132+
133+
input, err := model.CreateInput(
134+
openai.NewUserMessageFromParts(
135+
openai.NewTextContentPart("Describe this image."),
136+
openai.NewImageContentPartFromData(data, contentType),
137+
),
138+
)
139+
if err != nil {
140+
return nil, err
141+
}
142+
143+
output, err := model.Invoke(input)
144+
if err != nil {
145+
return nil, err
146+
}
147+
148+
// Return the image and its generated description.
149+
text := strings.TrimSpace(output.Choices[0].Message.Content)
150+
media := &Media{
151+
ContentType: contentType,
152+
Data: data,
153+
Text: text,
154+
}
155+
156+
return media, nil
157+
}
158+
159+
// This function fetches a random "Harvard Sentences" speech file from OpenSpeech, and then generates a transcript from it.
160+
// The sentences are from https://www.cs.columbia.edu/~hgs/audio/harvard.html
161+
func TranscribeRandomSpeech() (*Media, error) {
162+
163+
// Pick a random file number from the list of available here:
164+
// https://www.voiptroubleshooter.com/open_speech/american.html
165+
numbers := []int{10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 30, 31, 32, 34, 35, 36, 37, 38, 39, 40, 57, 58, 59, 60, 61}
166+
num := numbers[rand.IntN(len(numbers))]
167+
168+
// Fetch the speech file corresponding to the number.
169+
url := fmt.Sprintf("https://www.voiptroubleshooter.com/open_speech/american/OSR_us_000_%04d_8k.wav", num)
170+
response, err := http.Fetch(url)
171+
if err != nil {
172+
return nil, err
173+
}
174+
data := response.Body
175+
176+
// Transcribe the audio using an audio-enabled OpenAI chat model.
177+
model, err := models.GetModel[openai.ChatModel]("audio-model")
178+
if err != nil {
179+
return nil, err
180+
}
181+
182+
input, err := model.CreateInput(
183+
openai.NewDeveloperMessage("Do not include any newlines or surrounding quotation marks in the response. Omit any explanation beyond the request."),
184+
openai.NewUserMessageFromParts(
185+
openai.NewTextContentPart("Provide an exact transcription of the contents of this audio file."),
186+
openai.NewAudioContentPartFromData(data, "wav"),
187+
),
188+
)
189+
if err != nil {
190+
return nil, err
191+
}
192+
193+
output, err := model.Invoke(input)
194+
if err != nil {
195+
return nil, err
196+
}
197+
198+
// Return the audio file and its transcript.
199+
text := strings.TrimSpace(output.Choices[0].Message.Content)
200+
media := &Media{
201+
ContentType: "audio/wav",
202+
Data: data,
203+
Text: text,
204+
}
205+
206+
return media, nil
207+
}

0 commit comments

Comments
 (0)