├── LICENSE
├── README.md
├── chat_model.go
├── examples
├── README.md
├── calendar
│ ├── README.md
│ ├── main.go
│ └── models
│ │ ├── calendarActions.go
│ │ └── validator.go
├── interactive.go
└── openlibrary
│ ├── README.md
│ ├── client
│ └── client.go
│ ├── main.go
│ └── models
│ ├── search_request.go
│ └── search_response.go
├── go.mod
├── go.sum
├── openai_client.go
├── translator.go
├── type_definitions.go
└── validator.go
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Microsoft Corporation.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # go-typechat
2 |
3 | GoLang port of [microsoft/TypeChat](https://github.com/microsoft/TypeChat)
4 |
5 | ## What is TypeChat?
6 |
7 | From [microsoft/TypeChat](https://github.com/microsoft/TypeChat/tree/main#typechat)
8 |
9 | > TypeChat is a library that makes it easy to build natural language interfaces using types.
10 | >
11 | > Building natural language interfaces has traditionally been difficult. These apps often relied on complex decision trees to determine intent and collect the required inputs to take action. Large language models (LLMs) have made this easier by enabling us to take natural language input from a user and match to intent. This has introduced its own challenges including the need to constrain the model's reply for safety, structure responses from the model for further processing, and ensuring that the reply from the model is valid. Prompt engineering aims to solve these problems, but comes with a steep learning curve and increased fragility as the prompt increases in size.
12 | >
13 | > TypeChat replaces _prompt engineering_ with _schema engineering_.
14 | >
15 | > Simply define types that represent the intents supported in your natural language application. That could be as simple as an interface for categorizing sentiment or more complex examples like types for a shopping cart or music application. For example, to add additional intents to a schema, a developer can add additional types into a discriminated union. To make schemas hierarchical, a developer can use a "meta-schema" to choose one or more sub-schemas based on user input.
16 | >
17 | > After defining your types, TypeChat takes care of the rest by:
18 | >
19 | > 1. Constructing a prompt to the LLM using types.
20 | > 2. Validating the LLM response conforms to the schema. If the validation fails, repair the non-conforming output through further language model interaction.
21 | > 3. Summarizing succinctly (without use of a LLM) the instance and confirm that it aligns with user intent.
22 | >
23 | > Types are all you need!
24 |
25 | ## Getting Started
26 |
27 | Get the module:
28 |
29 | ```bash
30 | go get github.com/hrily/go-typechat
31 | ```
32 |
33 | ### Usage
34 |
35 |
36 | Translate Book Search Request
37 |
38 | ```golang
39 | package main
40 |
41 | import (
42 | "context"
43 | "fmt"
44 |
45 | "github.com/hrily/go-typechat"
46 | )
47 |
48 | // SearchRequest to search for books
49 | // The user can specify one or more of the following fields
50 | type SearchRequest struct {
51 | // Title will find any books with the given title
52 | Title string `json:"title,omitempty"`
53 | // Author will find any books with the given author
54 | Author string `json:"author,omitempty"`
55 | // Subject will find any books about the given subject
56 | // eg: "tennis rules" will find books about "tennis" and "rules"
57 | Subject string `json:"subject,omitempty"`
58 | // Query will find any books with the given query
59 | // Is used when the user input does not correspond to any of the other fields
60 | Query string `json:"query,omitempty"`
61 | }
62 |
63 | const searchRequestDefinition = "" +
64 | "// SearchRequest to search for books" +
65 | "// The user can specify one or more of the following fields" +
66 | "type SearchRequest struct {" +
67 | " // Title will find any books with the given title" +
68 | " Title string `json:\"title,omitempty\"`" +
69 | " // Author will find any books with the given author" +
70 | " Author string `json:\"author,omitempty\"`" +
71 | " // Subject will find any books about the given subject" +
72 | " // eg: \"tennis rules\" will find books about \"tennis\" and \"rules\"" +
73 | " Subject string `json:\"subject,omitempty\"`" +
74 | " // Query will find any books with the given query" +
75 | " // Is used when the user input does not correspond to any of the other fields" +
76 | " Query string `json:\"query,omitempty\"`" +
77 | "}"
78 |
79 | func main() {
80 | translator := typechat.NewTranslator(&typechat.TranslatorParams{
81 | ChatModel: typechat.NewOpenAIChatModel(),
82 | RepairAttempts: 0,
83 | TypeDefinitions: typechat.NewTypeDefinitions(searchRequestDefinition),
84 | })
85 |
86 | ctx := context.Background()
87 | request := "by JK Rowling"
88 |
89 | searchRequest := &SearchRequest{}
90 | if err := translator.Translate(
91 | ctx, request, searchRequest,
92 | ); err != nil {
93 | fmt.Println(err)
94 | return
95 | }
96 |
97 | // Will print:
98 | // &main.SearchRequest{Title:"", Author:"JK Rowling", Subject:"", Query:""}
99 | fmt.Printf("%#v\n", searchRequest)
100 | }
101 | ```
102 |
103 |
104 |
105 |
106 | Check [examples](examples/README.md) for more usage examples
107 |
--------------------------------------------------------------------------------
/chat_model.go:
--------------------------------------------------------------------------------
1 | package typechat
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 |
8 | "github.com/pkg/errors"
9 | openai "github.com/sashabaranov/go-openai"
10 | )
11 |
12 | // ChatModel is an interface to chat based language model
13 | type ChatModel interface {
14 | Send(ctx context.Context, messages []*ChatModelMessage) (string, error)
15 | }
16 |
17 | // ChatModelMessage is a message to send to the chat model
18 | // Invariant: one of System, User, or AI must be non-nil
19 | type ChatModelMessage struct {
20 | System *string
21 | User *string
22 | AI *string
23 | }
24 |
25 | type openAIChatModel struct {
26 | client *openai.Client
27 | model string
28 | }
29 |
30 | // NewOpenAIChatModel ...
31 | func NewOpenAIChatModel() ChatModel {
32 | model := openai.GPT3Dot5Turbo
33 | if modelEnv := os.Getenv("OPENAI_MODEL"); modelEnv != "" {
34 | model = modelEnv
35 | }
36 |
37 | client := NewOpenAIClient()
38 | return &openAIChatModel{
39 | client: client,
40 | model: model,
41 | }
42 | }
43 |
44 | // Send messages to the chat model and return the response
45 | func (m *openAIChatModel) Send(
46 | ctx context.Context, messages []*ChatModelMessage,
47 | ) (string, error) {
48 | msgs := make([]openai.ChatCompletionMessage, 0, len(messages))
49 | for _, message := range messages {
50 | openAIMessage, err := m.toOpenAIMessage(message)
51 | if err != nil {
52 | return "", errors.Wrap(err, "failed to create message")
53 | }
54 | msgs = append(msgs, *openAIMessage)
55 | }
56 |
57 | resp, err := m.client.CreateChatCompletion(
58 | ctx, openai.ChatCompletionRequest{
59 | Model: openai.GPT3Dot5Turbo,
60 | Messages: msgs,
61 | },
62 | )
63 |
64 | if err != nil {
65 | return "", errors.Wrap(err, "failed to send messages")
66 | }
67 | if len(resp.Choices) == 0 {
68 | return "", fmt.Errorf("no choices returned")
69 | }
70 |
71 | return resp.Choices[0].Message.Content, nil
72 | }
73 |
74 | func (m *openAIChatModel) toOpenAIMessage(
75 | message *ChatModelMessage,
76 | ) (*openai.ChatCompletionMessage, error) {
77 | switch {
78 | case message.System != nil:
79 | return &openai.ChatCompletionMessage{
80 | Role: openai.ChatMessageRoleSystem,
81 | Content: *message.System,
82 | }, nil
83 | case message.User != nil:
84 | return &openai.ChatCompletionMessage{
85 | Role: openai.ChatMessageRoleUser,
86 | Content: *message.User,
87 | }, nil
88 | case message.AI != nil:
89 | return &openai.ChatCompletionMessage{
90 | Role: openai.ChatMessageRoleAssistant,
91 | Content: *message.AI,
92 | }, nil
93 | default:
94 | return nil, fmt.Errorf("invalid message")
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | This directory contains examples to use `go-typechat`
4 |
5 | ## List of examples
6 |
7 | - [Calendar](calendar/README.md): This sample translates user intent into a sequence of actions to modify a calendar. Based on [microsoft/TypeChat/examples/calendar](https://github.com/microsoft/TypeChat/tree/main/examples/calendar).
8 | - [OpenLibrary](openlibrary/README.md): This sample translates user intent into [OpenLibrary Search API](https://openlibrary.org/dev/docs/api/search) request and calls it to get the result.
9 |
10 | ## Running examples
11 |
12 | 1. Export the following env variables
13 |
14 | ```bash
15 | export OPENAI_API_KEY="..."
16 | export OPENAI_MODEL="gpt-3.5-turbo"
17 | # if not using default organisation
18 | export OPENAI_ORGANIZATION="org-..."
19 | ```
20 |
21 | 2. Run the example from it's directory
22 |
23 | ```bash
24 | go run main.go
25 | ```
26 |
--------------------------------------------------------------------------------
/examples/calendar/README.md:
--------------------------------------------------------------------------------
1 | # Calendar go-typechat example
2 |
3 | This sample translates user intent into a sequence of actions to modify a calendar. Based on [microsoft/TypeChat/examples/calendar](https://github.com/microsoft/TypeChat/tree/main/examples/calendar).
4 |
5 | ## Try Calendar example
6 |
7 | To run the Calendar example, follow the instructions in the [examples README](https://github.com/Hrily/go-typechat/blob/main/examples/README.md#running-examples)
8 |
9 | ## Sample Runs
10 |
11 | ```
12 | $ go run main.go
13 | 📆> add meeting with Gavin at 1pm and remove meetings with Sasha
14 | {
15 | "actions": [
16 | {
17 | "addEvent": {
18 | "event": {
19 | "timeRange": {
20 | "startTime": "13:00"
21 | },
22 | "participants": [
23 | "Gavin"
24 | ]
25 | }
26 | }
27 | },
28 | {
29 | "removeEvent": {
30 | "eventReference": {
31 | "participants": [
32 | "Sasha"
33 | ]
34 | }
35 | }
36 | }
37 | ]
38 | }
39 | ```
40 |
--------------------------------------------------------------------------------
/examples/calendar/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 |
8 | "github.com/hrily/go-typechat"
9 | "github.com/hrily/go-typechat/examples"
10 | "github.com/hrily/go-typechat/examples/calendar/models"
11 | )
12 |
13 | var (
14 | translator = newTranslator(1)
15 | ctx = context.Background()
16 | )
17 |
18 | func main() {
19 | examples.Interactive(process, "📆> ")
20 | }
21 |
22 | func process(request string) {
23 | response := &models.CalendarActions{}
24 | if err := translator.Translate(
25 | ctx, request, response,
26 | ); err != nil {
27 | fmt.Println(err)
28 | return
29 | }
30 |
31 | j, err := json.MarshalIndent(response, "", " ")
32 | if err != nil {
33 | panic(err)
34 | }
35 | fmt.Println(string(j))
36 | }
37 |
38 | func newTranslator(repairAttempts int) typechat.Translator {
39 | definitions, err := typechat.NewTypeDefinitionsFromFile("models/calendarActions.go")
40 | if err != nil {
41 | panic(err)
42 | }
43 |
44 | return typechat.NewTranslator(&typechat.TranslatorParams{
45 | ChatModel: typechat.NewOpenAIChatModel(),
46 | RepairAttempts: repairAttempts,
47 | TypeDefinitions: definitions,
48 | })
49 | }
50 |
--------------------------------------------------------------------------------
/examples/calendar/models/calendarActions.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | type CalendarActions struct {
4 | Actions []*Action `json:"actions,omitempty"`
5 | }
6 |
7 | type Action struct {
8 | AddEvent *AddEventAction `json:"addEvent,omitempty"`
9 | RemoveEvent *RemoveEventAction `json:"removeEvent,omitempty"`
10 | FindEvents *FindEventsAction `json:"findEvents,omitempty"`
11 | AddParticipants *AddParticipantsAction `json:"addParticipants,omitempty"`
12 | Unknown *UnknownAction `json:"unknown,omitempty"`
13 | }
14 |
15 | type AddEventAction struct {
16 | Event *Event `json:"event,omitempty"`
17 | }
18 |
19 | type RemoveEventAction struct {
20 | EventReference *EventReference `json:"eventReference,omitempty"`
21 | }
22 |
23 | type FindEventsAction struct {
24 | // one or more event properties to use to search for matching events
25 | EventReference *EventReference `json:"eventReference,omitempty"`
26 | }
27 |
28 | type AddParticipantsAction struct {
29 | EventReference *EventReference `json:"eventReference,omitempty"`
30 | Participants []string `json:"participants,omitempty"`
31 | }
32 |
33 | // UnknownAction is used when the user types text that can not easily be
34 | // understood as a calendar action
35 | type UnknownAction struct {
36 | // Text typed by the user that the system did not understand
37 | Text string `json:"text,omitempty"`
38 | }
39 |
40 | type Event struct {
41 | // Day (example: March 22, 2024) or relative date (example: after EventReference)
42 | Day string `json:"day,omitempty"`
43 | TimeRange *EventTimeRange `json:"timeRange,omitempty"`
44 | Description string `json:"description,omitempty"`
45 | Location string `json:"location,omitempty"`
46 | // Participants is list of people or named groups like 'team'
47 | Participants []string `json:"participants,omitempty"`
48 | }
49 |
50 | // EventReference is properties used by the requester in referring to an event
51 | // these properties are only specified if given directly by the requester
52 | type EventReference struct {
53 | // Day (example: March 22, 2024) or relative date (example: after EventReference)
54 | Day string `json:"day,omitempty"`
55 | // DayRange (examples: this month, this week, in the next two days)
56 | DayRange string `json:"dayRange,omitempty"`
57 | TimeRange *EventTimeRange `json:"timeRange,omitempty"`
58 | Description string `json:"description,omitempty"`
59 | Location string `json:"location,omitempty"`
60 | Participants []string `json:"participants,omitempty"`
61 | }
62 |
63 | type EventTimeRange struct {
64 | StartTime string `json:"startTime,omitempty"`
65 | EndTime string `json:"endTime,omitempty"`
66 | Duration string `json:"duration,omitempty"`
67 | }
68 |
--------------------------------------------------------------------------------
/examples/calendar/models/validator.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import "github.com/hrily/go-typechat"
4 |
5 | func (c *CalendarActions) Validate() error {
6 | for _, actions := range c.Actions {
7 | if err := actions.Validate(); err != nil {
8 | return err
9 | }
10 | }
11 | return nil
12 | }
13 |
14 | func (a *Action) Validate() error {
15 | if a.AddEvent != nil {
16 | return a.AddEvent.Validate()
17 | }
18 | return nil
19 | }
20 |
21 | func (a *AddEventAction) Validate() error {
22 | if a.Event != nil {
23 | return a.Event.Validate()
24 | }
25 | return nil
26 | }
27 |
28 | func (e *Event) Validate() error {
29 | if e.TimeRange != nil {
30 | return e.TimeRange.Validate()
31 | }
32 | return nil
33 | }
34 |
35 | var (
36 | invalidTimeRange = typechat.ValidationError{
37 | Message: "invalid time range: required format is HH:MM in 24 hour format",
38 | }
39 | )
40 |
41 | func (t *EventTimeRange) Validate() error {
42 | // very basic validation
43 | if len(t.StartTime) != 5 {
44 | return invalidTimeRange
45 | }
46 | if len(t.EndTime) != 5 {
47 | return invalidTimeRange
48 | }
49 | return nil
50 | }
51 |
--------------------------------------------------------------------------------
/examples/interactive.go:
--------------------------------------------------------------------------------
1 | package examples
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "os"
7 | )
8 |
9 | func Interactive(
10 | process func(request string),
11 | prompt string,
12 | ) {
13 | for {
14 | fmt.Print(prompt)
15 |
16 | in := bufio.NewReader(os.Stdin)
17 | request, err := in.ReadString('\n')
18 | if err != nil {
19 | panic(err)
20 | }
21 |
22 | process(request)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/examples/openlibrary/README.md:
--------------------------------------------------------------------------------
1 | # OpenLibrary go-typechat example
2 |
3 | This sample translates user intent into [OpenLibrary Search API](https://openlibrary.org/dev/docs/api/search) request and calls it to get the result.
4 |
5 | ## Try OpenLibrary example
6 |
7 | To run the OpenLibrary example, follow the instructions in the [examples README](https://github.com/Hrily/go-typechat/blob/main/examples/README.md#running-examples)
8 |
9 | ## Sample Runs
10 |
11 | ```
12 | $ go run main.go
13 | 📖> books about self help
14 | Searching: https://openlibrary.org/search.json?sort=rating&subject=self+help
15 | + A child called "it" - Dave Pelzer
16 | + Atomic Habits - James Clear, Àlex Guàrdia i Berdiell
17 | + The 48 Laws of Power - Robert Greene
18 | + How to Win Friends and Influence People - Dale Carnegie
19 | + The Fault in Our Stars - John Green
20 |
21 | 📖> by robert green
22 | Searching: https://openlibrary.org/search.json?author=robert+green&sort=rating
23 | + The 48 Laws of Power - Robert Greene
24 | + AS 48 LEIS DO PODER - Robert Greene
25 | + The Laws of Human Nature - Robert Greene
26 | + Las 48 leyes del poder - Robert Greene
27 | + Las 48 Leyes del Poder - Robert Greene
28 |
29 | 📖> book named human nature
30 | Searching: https://openlibrary.org/search.json?sort=rating&title=human+nature
31 | + The Laws of Human Nature - Robert Greene
32 | + The Nature Of Human Values - Milton Rokeach
33 | + A Treatise of Human Nature - David Hume
34 | + The Red Queen: Sex and the Evolution of Human Nature -
35 | + On human nature - Edward Osborne Wilson
36 |
37 |
38 | ```
39 |
--------------------------------------------------------------------------------
/examples/openlibrary/client/client.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "io/ioutil"
8 | "net/http"
9 | "net/url"
10 |
11 | "github.com/hrily/go-typechat/examples/openlibrary/models"
12 | "github.com/pkg/errors"
13 | )
14 |
15 | const (
16 | openLibraryURL = "https://openlibrary.org"
17 | searchPath = "/search.json"
18 | searchURL = openLibraryURL + searchPath
19 | )
20 |
21 | type Client interface {
22 | Search(ctx context.Context, request *models.SearchRequest) (*models.SearchResponse, error)
23 | }
24 |
25 | type client struct{}
26 |
27 | func New() Client {
28 | return &client{}
29 | }
30 |
31 | func (c *client) Search(
32 | ctx context.Context, request *models.SearchRequest,
33 | ) (*models.SearchResponse, error) {
34 | params := url.Values{}
35 | params.Add("sort", "rating")
36 | if request.Title != nil {
37 | params.Add("title", *request.Title)
38 | }
39 | if request.Author != nil {
40 | params.Add("author", *request.Author)
41 | }
42 | if request.Subject != nil {
43 | params.Add("subject", *request.Subject)
44 | }
45 | if request.Query != nil {
46 | params.Add("q", *request.Query)
47 | }
48 |
49 | u, _ := url.Parse(searchURL)
50 | u.RawQuery = params.Encode()
51 |
52 | fmt.Println("Searching: ", u.String())
53 |
54 | resp, err := http.Get(u.String())
55 | if err != nil {
56 | return nil, errors.Wrap(err, "failed to get search response")
57 | }
58 |
59 | body, err := ioutil.ReadAll(resp.Body)
60 | if err != nil {
61 | return nil, errors.Wrap(err, "failed to read search response body")
62 | }
63 |
64 | response := &models.SearchResponse{}
65 | err = json.Unmarshal(body, response)
66 | if err != nil {
67 | return nil, errors.Wrap(err, "failed to parse search response")
68 | }
69 |
70 | return response, nil
71 | }
72 |
--------------------------------------------------------------------------------
/examples/openlibrary/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/hrily/go-typechat"
9 | "github.com/hrily/go-typechat/examples"
10 | "github.com/hrily/go-typechat/examples/openlibrary/client"
11 | "github.com/hrily/go-typechat/examples/openlibrary/models"
12 | )
13 |
14 | const (
15 | maxBooksToPrint = 5
16 | )
17 |
18 | var (
19 | openlibrary = client.New()
20 | translator = newTranslator(1)
21 | ctx = context.Background()
22 | )
23 |
24 | func main() {
25 | examples.Interactive(process, "📖> ")
26 | }
27 |
28 | func process(request string) {
29 | searchRequest := &models.SearchRequest{}
30 | if err := translator.Translate(
31 | ctx, request, searchRequest,
32 | ); err != nil {
33 | fmt.Println(err)
34 | return
35 | }
36 |
37 | response, err := openlibrary.Search(ctx, searchRequest)
38 | if err != nil {
39 | panic(err)
40 | }
41 |
42 | printResponse(response)
43 | fmt.Println()
44 | }
45 |
46 | func printResponse(response *models.SearchResponse) {
47 | if len(response.Books) == 0 {
48 | fmt.Println("No books found")
49 | return
50 | }
51 | if len(response.Books) > maxBooksToPrint {
52 | response.Books = response.Books[:maxBooksToPrint]
53 | }
54 |
55 | for _, book := range response.Books {
56 | fmt.Println("+ ", book.Title, " - ", strings.Join(book.Authors, ", "))
57 | }
58 | }
59 |
60 | func newTranslator(repairAttempts int) typechat.Translator {
61 | definitions, err := typechat.NewTypeDefinitionsFromFile("models/search_request.go")
62 | if err != nil {
63 | panic(err)
64 | }
65 |
66 | return typechat.NewTranslator(&typechat.TranslatorParams{
67 | ChatModel: typechat.NewOpenAIChatModel(),
68 | RepairAttempts: repairAttempts,
69 | TypeDefinitions: definitions,
70 | })
71 | }
72 |
--------------------------------------------------------------------------------
/examples/openlibrary/models/search_request.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import "github.com/hrily/go-typechat"
4 |
5 | // SearchRequest to search for books
6 | // The user can specify one or more of the following fields
7 | type SearchRequest struct {
8 | // Title will find any books with the given title
9 | Title *string `json:"title,omitempty"`
10 | // Author will find any books with the given author
11 | Author *string `json:"author,omitempty"`
12 | // Subject will find any books about the given subject
13 | // eg: "tennis rules" will find books about "tennis" and "rules"
14 | Subject *string `json:"subject,omitempty"`
15 | // Query will find any books with the given query
16 | // Is used when the user input does not correspond to any of the other fields
17 | Query *string `json:"query,omitempty"`
18 | }
19 |
20 | func (s *SearchRequest) Validate() error {
21 | if s.Title == nil && s.Author == nil && s.Subject == nil && s.Query == nil {
22 | return &typechat.ValidationError{
23 | Message: "At least one of title, author, subject, or query must be specified",
24 | }
25 | }
26 | return nil
27 | }
28 |
--------------------------------------------------------------------------------
/examples/openlibrary/models/search_response.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | type SearchResponse struct {
4 | Books []*Book `json:"docs,omitempty"`
5 | }
6 |
7 | type Book struct {
8 | Title string `json:"title,omitempty"`
9 | Authors []string `json:"author_name,omitempty"`
10 | }
11 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/hrily/go-typechat
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/pkg/errors v0.9.1
7 | github.com/sashabaranov/go-openai v1.14.1
8 | )
9 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
2 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
3 | github.com/sashabaranov/go-openai v1.14.1 h1:jqfkdj8XHnBF84oi2aNtT8Ktp3EJ0MfuVjvcMkfI0LA=
4 | github.com/sashabaranov/go-openai v1.14.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
5 |
--------------------------------------------------------------------------------
/openai_client.go:
--------------------------------------------------------------------------------
1 | package typechat
2 |
3 | import (
4 | "os"
5 |
6 | openai "github.com/sashabaranov/go-openai"
7 | )
8 |
9 | // NewOpenAIClient ...
10 | func NewOpenAIClient() *openai.Client {
11 | apiKey := os.Getenv("OPENAI_API_KEY")
12 | config := openai.DefaultConfig(apiKey)
13 | if orgID := os.Getenv("OPENAI_ORGANIZATION"); orgID != "" {
14 | config.OrgID = orgID
15 | }
16 | client := openai.NewClientWithConfig(config)
17 |
18 | return client
19 | }
20 |
--------------------------------------------------------------------------------
/translator.go:
--------------------------------------------------------------------------------
1 | package typechat
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "reflect"
8 |
9 | "github.com/pkg/errors"
10 | )
11 |
12 | const (
13 | requestPromptFormat = `You are a service that translates user requests into JSON objects of type "%s" according to the following GoLang definitions:
14 | """
15 | %s
16 | """
17 |
18 | Return the user request translated into a JSON object with 2 spaces of indentation
19 |
20 | Ensure the following for JSON:
21 | - omit any fields with null or undefined values
22 | `
23 | repairPromptFormat = `The JSON object is invalid for the following reason:
24 | """
25 | %s
26 | """
27 | Return revised JSON object`
28 | )
29 |
30 | // Translator ...
31 | type Translator interface {
32 | Translate(ctx context.Context, request string, target interface{}) error
33 | }
34 |
35 | type translator struct {
36 | *TranslatorParams
37 | }
38 |
39 | // TranslatorParams are the dependencies for Translator
40 | type TranslatorParams struct {
41 | ChatModel ChatModel
42 | RepairAttempts int
43 | TypeDefinitions *TypeDefinitions
44 | }
45 |
46 | // NewTranslator ...
47 | func NewTranslator(params *TranslatorParams) Translator {
48 | return &translator{
49 | TranslatorParams: params,
50 | }
51 | }
52 |
53 | // Translate a natural language `request` into JSON and unmarshal it into `target`
54 | // if the JSON returned by the language model is not valid and max
55 | // `RepairAttempts` will be made to repair the JSON using diagnostics produced
56 | // in validation.
57 | // If the JSON is not valid after `RepairAttempts` attempts, an error will be
58 | // returned.
59 | // `target` can optionally implement `Validator` to perform additional
60 | // validation on the response, whose error will be used in diagnostics to
61 | // repair the response.
62 | func (t *translator) Translate(
63 | ctx context.Context, request string, target interface{},
64 | ) (err error) {
65 | requestPrompt := t.createRequestPrompt(t.TypeDefinitions.definitions, target)
66 | messages := []*ChatModelMessage{
67 | {System: &requestPrompt},
68 | {User: &request},
69 | }
70 |
71 | for attempts := t.RepairAttempts + 1; attempts > 0; attempts-- {
72 | response, err := t.ChatModel.Send(ctx, messages)
73 | if err != nil {
74 | return err
75 | }
76 |
77 | if err := json.Unmarshal([]byte(response), target); err != nil {
78 | messages = t.appendRepairDiagnostics(
79 | messages, response, errors.Wrap(err, "malformed json"),
80 | )
81 | continue
82 | }
83 |
84 | validator, ok := target.(Validator)
85 | if !ok {
86 | return nil
87 | }
88 |
89 | err = validator.Validate()
90 | if err == nil {
91 | return nil
92 | }
93 |
94 | messages = t.appendRepairDiagnostics(messages, response, err)
95 | }
96 |
97 | return
98 | }
99 |
100 | func (t *translator) createRequestPrompt(
101 | request string, target interface{},
102 | ) string {
103 | typeName := t.getTypeName(target)
104 | return fmt.Sprintf(requestPromptFormat, typeName, request)
105 | }
106 |
107 | func (t *translator) getTypeName(myvar interface{}) string {
108 | if t := reflect.TypeOf(myvar); t.Kind() == reflect.Ptr {
109 | return t.Elem().Name()
110 | } else {
111 | return t.Name()
112 | }
113 | }
114 |
115 | func (t *translator) appendRepairDiagnostics(
116 | messages []*ChatModelMessage, response string, err error,
117 | ) []*ChatModelMessage {
118 | repairPrompt := t.createRepairPrompt(err)
119 | messages = append(messages,
120 | &ChatModelMessage{AI: &response},
121 | &ChatModelMessage{User: &repairPrompt},
122 | )
123 | return messages
124 | }
125 |
126 | func (t *translator) createRepairPrompt(err error) string {
127 | return fmt.Sprintf(repairPromptFormat, err.Error())
128 | }
129 |
--------------------------------------------------------------------------------
/type_definitions.go:
--------------------------------------------------------------------------------
1 | package typechat
2 |
3 | import (
4 | "bytes"
5 | "io/ioutil"
6 | "os"
7 |
8 | "github.com/pkg/errors"
9 | )
10 |
11 | // TypeDefinitions are the GoLang type definitions used to translate natural
12 | // language into JSON
13 | type TypeDefinitions struct {
14 | definitions string
15 | }
16 |
17 | // NewTypeDefinitions from given definitions string
18 | func NewTypeDefinitions(definitions string) *TypeDefinitions {
19 | return &TypeDefinitions{
20 | definitions: definitions,
21 | }
22 | }
23 |
24 | // NewTypeDefinitionsFromFile reads type definitions from given files
25 | func NewTypeDefinitionsFromFile(files ...string) (*TypeDefinitions, error) {
26 | buffer := bytes.NewBuffer([]byte{})
27 | for _, filename := range files {
28 | file, err := os.Open(filename)
29 | if err != nil {
30 | return nil, errors.Wrap(err, "failed to open type definitions file")
31 | }
32 |
33 | defer file.Close()
34 | bytes, err := ioutil.ReadAll(file)
35 | if err != nil {
36 | return nil, errors.Wrap(err, "failed to read type definitions file")
37 | }
38 | buffer.Write(bytes)
39 | }
40 |
41 | return NewTypeDefinitions(buffer.String()), nil
42 | }
43 |
--------------------------------------------------------------------------------
/validator.go:
--------------------------------------------------------------------------------
1 | package typechat
2 |
3 | // Validator to perform additional validations on the response
4 | type Validator interface {
5 | // Validate the response and return `ValidationError` if it fails validation
6 | Validate() error
7 | }
8 |
9 | // ValidationError is an error returned when validation fails
10 | // `Message` will be used in diagnostics to repair the response
11 | type ValidationError struct {
12 | Message string
13 | }
14 |
15 | // Ensure ValidationError implements error
16 | var _ error = ValidationError{}
17 |
18 | // Error ...
19 | func (e ValidationError) Error() string {
20 | return e.Message
21 | }
22 |
--------------------------------------------------------------------------------