├── 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 | --------------------------------------------------------------------------------