├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README-zh_CN.md ├── README.md ├── examples ├── README.md ├── calendar │ ├── README.md │ ├── calendar_actions_schema.go │ ├── go.mod │ ├── input.txt │ └── main.go ├── coffee_shop │ └── README.md ├── math │ ├── README.md │ ├── go.mod │ ├── go.sum │ ├── input.txt │ ├── main.go │ ├── math_schema.go │ └── run.png ├── music │ └── README.md ├── restaurant │ ├── README.md │ ├── food_order_view_schema.go │ ├── go.mod │ ├── input.txt │ ├── main.go │ └── run.png └── sentiment │ ├── README.md │ ├── go.mod │ ├── go.sum │ ├── input.txt │ ├── main.go │ ├── run.png │ └── sentiment_schema.go ├── go.mod ├── interactive.go ├── model.go ├── program.go ├── program_schema.tpl ├── typechat.go └── validate.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Go package 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | 12 | - name: Set up Go 13 | uses: actions/setup-go@v3 14 | with: 15 | go-version: 1.18 16 | 17 | - name: Tidy 18 | run: go mod tidy 19 | 20 | - name: Build 21 | run: go build -v ./... 22 | 23 | - name: Test 24 | run: go test -v ./... -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .env 3 | 4 | # Local development and debugging 5 | .vscode 6 | **/.vscode/* 7 | **/tsconfig.debug.json 8 | !**/.vscode/launch.json 9 | **/build.bat 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 John Mai 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-zh_CN.md: -------------------------------------------------------------------------------- 1 | [English](./README.md) | 简体中文 2 | 3 | 4 | 5 | 6 | 7 | [![go report card][go-report-card]][go-report-card-url] 8 | [![Go.Dev reference][go.dev-reference]][go.dev-reference-url] 9 | [![Go package][go-pacakge]][go-pacakge-url] 10 | [![MIT License][license-shield]][license-url] 11 | [![Contributors][contributors-shield]][contributors-url] 12 | [![Forks][forks-shield]][forks-url] 13 | [![Stargazers][stars-shield]][stars-url] 14 | [![Issues][issues-shield]][issues-url] 15 | 16 | # TypeChat-Go 17 | 18 | 这是 [microsoft/TypeChat](https://github.com/microsoft/TypeChat) 的 Go 语言实现。 19 | 20 | TypeChat-Go 是一个库,它通过 Schema 的方式实现 Prompt,以替代自然语言方式。 21 | 22 | ⭐️ 点个star支持我们的工作吧! 23 | 24 | # 入门指南 25 | 26 | 安装 TypeChat-Go: 27 | 28 | ```bash 29 | go get github.com/maiqingqiang/typechat-go 30 | ``` 31 | 32 | 配置环境变量 33 | 34 | 目前,示例可以使用 OpenAI 或 Azure OpenAI。 35 | 要使用 OpenAI,请使用以下环境变量: 36 | 37 | | 环境变量 | 值 | 38 | |-----------------------|--------------------------------------------------------------------------| 39 | | `OPENAI_MODEL` | OpenAI 模型名称(例如 gpt-3.5-turbo 或 gpt-4) | 40 | | `OPENAI_API_KEY` | 你的 OpenAI 密钥 | 41 | | `OPENAI_ENDPOINT` | OpenAI API 节点 - *可选*, 默认 `"https://api.openai.com/v1/chat/completions"` | 42 | | `OPENAI_ORGANIZATION` | OpenAI Organization - *可选*, 默认 `""` | 43 | 44 | 要使用 Azure OpenAI,请使用以下环境变量: 45 | 46 | | 环境变量 | 值 | 47 | |-------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| 48 | | `AZURE_OPENAI_ENDPOINT` | Azure OpenAI REST API 的完整 URL (e.g. `https://YOUR_RESOURCE_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT_NAME/chat/completions?api-version=2023-05-15`) | 49 | | `AZURE_OPENAI_API_KEY` | 你的 Azure OpenAI 密钥 | 50 | 51 | # 示例 52 | 53 | 要了解 TypeChat-Go 的实际效果,请查看此[examples](./examples)目录中的示例。 54 | 55 | 每个示例展示了 TypeChat-Go 如何处理自然语言输入,并将其映射为经过验证的 JSON 输出。大多数示例输入都适用于 GPT 3.5 和 GPT 4。 56 | 57 | | 示例名称 | 描述 | 58 | |----------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------| 59 | | [情感分析](https://github.com/maiqingqiang/TypeChat-Go/tree/main/examples/sentiment) | 一种情感分类器,将用户输入分类为负面、中性或积极。这是 TypeChat-Go 的 "hello world!" | 60 | | [咖啡店](https://github.com/maiqingqiang/TypeChat-Go/tree/main/examples/coffeeShop) | TODO | 61 | | [日历](https://github.com/maiqingqiang/TypeChat-Go/tree/main/examples/calendar) | 智能调度程序。此示例将用户意图转化为一系列的操作来修改日历。 | 62 | | [餐厅](https://github.com/maiqingqiang/TypeChat-Go/tree/main/examples/restaurant) | 一个用于在餐厅接受订单的智能代理。类似于咖啡店示例,但使用了更复杂的架构来建模更复杂的语言输入。散文文件说明了在处理复合句、干扰和更正时,简单和高级语言模型之间的界限。此示例还展示了如何使用 Go 定义来描述用户意图。 | 63 | | [数学计算](https://github.com/maiqingqiang/TypeChat-Go/tree/main/examples/math) | 将计算转化为可以执行四个基本数学运算符的 API 给定的简单程序。此示例突显了 TypeChat-Go 的程序生成能力。 | 64 | | [音乐](https://github.com/maiqingqiang/TypeChat-Go/tree/main/examples/music) | TODO | 65 | 66 | ## 运行示例 67 | 68 | 运行其中一个示例,可以在其目录下运行 `go run . `。 69 | F例如,在 [math](./examples/math) 目录中,你可以运行: 70 | 71 | ``` 72 | go run . /input.txt 73 | ``` 74 | 75 | ![run.png](./examples/math/run.png) 76 | 77 | 78 | 79 | [contributors-shield]: https://img.shields.io/github/contributors/maiqingqiang/TypeChat-Go.svg 80 | [contributors-url]: https://github.com/maiqingqiang/TypeChat-Go/graphs/contributors 81 | [forks-shield]: https://img.shields.io/github/forks/maiqingqiang/TypeChat-Go.svg 82 | [forks-url]: https://github.com/maiqingqiang/TypeChat-Go/network/members 83 | [stars-shield]: https://img.shields.io/github/stars/maiqingqiang/TypeChat-Go.svg 84 | [stars-url]: https://github.com/maiqingqiang/TypeChat-Go/stargazers 85 | [issues-shield]: https://img.shields.io/github/issues/maiqingqiang/TypeChat-Go.svg 86 | [issues-url]: https://github.com/maiqingqiang/TypeChat-Go/issues 87 | [license-shield]: https://img.shields.io/github/license/maiqingqiang/TypeChat-Go.svg 88 | [license-url]: https://github.com/maiqingqiang/TypeChat-Go/blob/main/LICENSE 89 | [go-report-card]: https://goreportcard.com/badge/github.com/maiqingqiang/typechat-go 90 | [go-report-card-url]: https://goreportcard.com/report/github.com/maiqingqiang/typechat-go 91 | [go.dev-reference]: https://img.shields.io/badge/go.dev-reference-blue?logo=go&logoColor=white 92 | [go.dev-reference-url]: https://pkg.go.dev/github.com/maiqingqiang/typechat-go?tab=doc 93 | [go-pacakge]: https://github.com/maiqingqiang/TypeChat-Go/actions/workflows/test.yml/badge.svg?branch=main 94 | [go-pacakge-url]: https://github.com/maiqingqiang/TypeChat-Go/actions/workflows/test.yml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | English | [简体中文](./README-zh_CN.md) 2 | 3 | 4 | 5 | 6 | 7 | [![go report card][go-report-card]][go-report-card-url] 8 | [![Go.Dev reference][go.dev-reference]][go.dev-reference-url] 9 | [![Go package][go-pacakge]][go-pacakge-url] 10 | [![MIT License][license-shield]][license-url] 11 | [![Contributors][contributors-shield]][contributors-url] 12 | [![Forks][forks-shield]][forks-url] 13 | [![Stargazers][stars-shield]][stars-url] 14 | [![Issues][issues-shield]][issues-url] 15 | 16 | # TypeChat-Go 17 | 18 | This is the Go language implementation of [microsoft/TypeChat](https://github.com/microsoft/TypeChat). 19 | 20 | TypeChat-Go is a library that makes it easy to build natural language interfaces using types. 21 | 22 | > [microsoft/TypeChat](https://github.com/microsoft/TypeChat#typechat): Building natural language interfaces has traditionally 23 | > been difficult. These apps often relied on complex decision trees to determine intent and collect the required inputs 24 | > to 25 | > take action. Large language models (LLMs) have made this easier by enabling us to take natural language input from a 26 | > user and match to intent. This has introduced its own challenges including the need to constrain the model's reply for 27 | > safety, structure responses from the model for further processing, and ensuring that the reply from the model is 28 | > valid. 29 | > Prompt engineering aims to solve these problems, but comes with a steep learning curve and increased fragility as the 30 | > prompt increases in size. 31 | > TypeChat replaces prompt engineering with schema engineering. 32 | > Simply define types that represent the intents supported in your natural language application. That could be as simple 33 | > as an interface for categorizing sentiment or more complex examples like types for a shopping cart or music 34 | > application. 35 | > For example, to add additional intents to a schema, a developer can add additional types into a discriminated union. 36 | > To 37 | > make schemas hierarchical, a developer can use a "meta-schema" to choose one or more sub-schemas based on user input. 38 | > 39 | > After defining your types, TypeChat takes care of the rest by: 40 | > 1. Constructing a prompt to the LLM using types. 41 | > 2. Validating the LLM response conforms to the schema. If the validation fails, repair the non-conforming output 42 | through further language model interaction. 43 | > 3. Summarizing succinctly (without use of a LLM) the instance and confirm that it aligns with user intent. 44 | > 45 | > Types are all you need! 46 | 47 | ⭐️ Star to support our work! 48 | 49 | # Getting Started 50 | 51 | Install TypeChat-Go: 52 | 53 | ```bash 54 | go get github.com/maiqingqiang/typechat-go 55 | ``` 56 | 57 | Configure environment variables 58 | 59 | Currently, the examples are running on OpenAI or Azure OpenAI endpoints. 60 | To use an OpenAI endpoint, include the following environment variables: 61 | 62 | | Variable | Value | 63 | |-----------------------|-----------------------------------------------------------------------------------------------| 64 | | `OPENAI_MODEL` | The OpenAI model name (e.g. `gpt-3.5-turbo` or `gpt-4`) | 65 | | `OPENAI_API_KEY` | Your OpenAI API key | 66 | | `OPENAI_ENDPOINT` | OpenAI API Endpoint - *optional*, defaults to `"https://api.openai.com/v1/chat/completions"` | 67 | | `OPENAI_ORGANIZATION` | OpenAI Organization - *optional*, defaults to `""` | 68 | 69 | To use an Azure OpenAI endpoint, include the following environment variables: 70 | 71 | | Variable | Value | 72 | |-------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 73 | | `AZURE_OPENAI_ENDPOINT` | The full URL of the Azure OpenAI REST API (e.g. `https://YOUR_RESOURCE_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT_NAME/chat/completions?api-version=2023-05-15`) | 74 | | `AZURE_OPENAI_API_KEY` | Your Azure OpenAI API key | 75 | 76 | # Examples 77 | 78 | To see TypeChat-Go in action, check out the [examples](./examples) found in this directory. 79 | 80 | Each example shows how TypeChat-Go handles natural language input, and maps to validated JSON as output. Most example 81 | inputs run on both GPT 3.5 and GPT 4. 82 | 83 | | Name | Description | 84 | |------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 85 | | [Sentiment](https://github.com/maiqingqiang/TypeChat-Go/tree/main/examples/sentiment) | A sentiment classifier which categorizes user input as negative, neutral, or positive. This is TypeChat-Go's "hello world!" | 86 | | [Coffee Shop](https://github.com/maiqingqiang/TypeChat-Go/tree/main/examples/coffeeShop) | TODO | 87 | | [Calendar](https://github.com/maiqingqiang/TypeChat-Go/tree/main/examples/calendar) | An intelligent scheduler. This sample translates user intent into a sequence of actions to modify a calendar. | 88 | | [Restaurant](https://github.com/maiqingqiang/TypeChat-Go/tree/main/examples/restaurant) | An intelligent agent for taking orders at a restaurant. Similar to the coffee shop example, but uses a more complex schema to model more complex linguistic input. The prose files illustrate the line between simpler and more advanced language models in handling compound sentences, distractions, and corrections. This example also shows how we can use Go to provide a user intent summary. | 89 | | [Math](https://github.com/maiqingqiang/TypeChat-Go/tree/main/examples/math) | Translate calculations into simple programs given an API that can perform the 4 basic mathematical operators. This example highlights TypeChat-Go's program generation capabilities. | 90 | | [Music](https://github.com/maiqingqiang/TypeChat-Go/tree/main/examples/music) | TODO | 91 | 92 | ## Run the examples 93 | 94 | To run an example with one of these input files, run `go run . `. 95 | For example, in the [math](./examples/math) directory, you can run: 96 | 97 | ``` 98 | go run . /input.txt 99 | ``` 100 | 101 | ![run.png](./examples/math/run.png) 102 | 103 | 104 | 105 | [contributors-shield]: https://img.shields.io/github/contributors/maiqingqiang/TypeChat-Go.svg 106 | [contributors-url]: https://github.com/maiqingqiang/TypeChat-Go/graphs/contributors 107 | [forks-shield]: https://img.shields.io/github/forks/maiqingqiang/TypeChat-Go.svg 108 | [forks-url]: https://github.com/maiqingqiang/TypeChat-Go/network/members 109 | [stars-shield]: https://img.shields.io/github/stars/maiqingqiang/TypeChat-Go.svg 110 | [stars-url]: https://github.com/maiqingqiang/TypeChat-Go/stargazers 111 | [issues-shield]: https://img.shields.io/github/issues/maiqingqiang/TypeChat-Go.svg 112 | [issues-url]: https://github.com/maiqingqiang/TypeChat-Go/issues 113 | [license-shield]: https://img.shields.io/github/license/maiqingqiang/TypeChat-Go.svg 114 | [license-url]: https://github.com/maiqingqiang/TypeChat-Go/blob/main/LICENSE 115 | [go-report-card]: https://goreportcard.com/badge/github.com/maiqingqiang/typechat-go 116 | [go-report-card-url]: https://goreportcard.com/report/github.com/maiqingqiang/typechat-go 117 | [go.dev-reference]: https://img.shields.io/badge/go.dev-reference-blue?logo=go&logoColor=white 118 | [go.dev-reference-url]: https://pkg.go.dev/github.com/maiqingqiang/typechat-go?tab=doc 119 | [go-pacakge]: https://github.com/maiqingqiang/TypeChat-Go/actions/workflows/test.yml/badge.svg?branch=main 120 | [go-pacakge-url]: https://github.com/maiqingqiang/TypeChat-Go/actions/workflows/test.yml -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Run the examples 2 | 3 | To run an example with one of these input files, run `go run . `. 4 | 5 | you can run: 6 | ``` 7 | go run . /input.txt 8 | ``` -------------------------------------------------------------------------------- /examples/calendar/README.md: -------------------------------------------------------------------------------- 1 | # Run the calendar example 2 | 3 | To run an example with one of these input files, run `go run . `. 4 | 5 | you can run: 6 | ``` 7 | go run . /input.txt 8 | ``` -------------------------------------------------------------------------------- /examples/calendar/calendar_actions_schema.go: -------------------------------------------------------------------------------- 1 | // The following types define the structure of an object of type CalendarActions that represents a list of requested calendar actions 2 | package main 3 | 4 | type CalendarActions struct { 5 | Actions []Action 6 | } 7 | 8 | type Action struct { 9 | AddEventAction *AddEventAction `json:"add_event,omitempty"` 10 | RemoveEventAction *RemoveEventAction `json:"remove_event,omitempty"` 11 | AddParticipantsAction *AddParticipantsAction `json:"add_participants,omitempty"` 12 | ChangeTimeRangeAction *ChangeTimeRangeAction `json:"change_time_range,omitempty"` 13 | ChangeDescriptionAction *ChangeDescriptionAction `json:"change_description,omitempty"` 14 | FindEventsAction *FindEventsAction `json:"find_events,omitempty"` 15 | UnknownAction *UnknownAction `json:"unknown,omitempty"` 16 | } 17 | 18 | type AddEventAction struct { 19 | Event *Event 20 | } 21 | 22 | type RemoveEventAction struct { 23 | EventReference *EventReference 24 | } 25 | 26 | type AddParticipantsAction struct { 27 | // event to be augmented; if not specified assume last event discussed 28 | EventReference *EventReference `json:"event_reference,omitempty"` 29 | // new participants (one or more) 30 | Participants []string 31 | } 32 | 33 | type ChangeTimeRangeAction struct { 34 | // event to be changed 35 | EventReference *EventReference 36 | // new time range for the event 37 | TimeRange *EventTimeRange 38 | } 39 | 40 | type ChangeDescriptionAction struct { 41 | // event to be changed 42 | EventReference *EventReference `json:"event_reference,omitempty"` 43 | // new description for the event 44 | Description string 45 | } 46 | 47 | type FindEventsAction struct { 48 | // one or more event properties to use to search for matching events 49 | EventReference *EventReference 50 | } 51 | 52 | // UnknownAction if the user types text that can not easily be understood as a calendar action, this action is used 53 | type UnknownAction struct { 54 | // text typed by the user that the system did not understand 55 | Text string 56 | } 57 | 58 | type EventTimeRange struct { 59 | StartTime string `json:"start_time,omitempty"` 60 | EndTime string `json:"end_time,omitempty"` 61 | Duration string `json:"duration,omitempty"` 62 | } 63 | 64 | type Event struct { 65 | // date (example: March 22, 2024) or relative date (example: after EventReference) 66 | Day string 67 | TimeRange *EventTimeRange 68 | Description string 69 | Location string `json:"location,omitempty"` 70 | // a list of people or named groups like 'team' 71 | Participants []string `json:"participants,omitempty"` 72 | } 73 | 74 | // EventReference properties used by the requester in referring to an event 75 | // these properties are only specified if given directly by the requester 76 | type EventReference struct { 77 | // date (example: March 22, 2024) or relative date (example: after EventReference) 78 | Day string `json:"day,omitempty"` 79 | // (examples: this month, this week, in the next two days) 80 | DayRange string `json:"day_range,omitempty"` 81 | TimeRange *EventTimeRange `json:"time_range,omitempty"` 82 | Description string `json:"description,omitempty"` 83 | Location string `json:"location,omitempty"` 84 | Participants []string `json:"participants,omitempty"` 85 | } 86 | -------------------------------------------------------------------------------- /examples/calendar/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/maiqingqiang/examples/calendar 2 | 3 | go 1.20 4 | 5 | require github.com/maiqingqiang/typechat-go v1.0.0 6 | 7 | replace github.com/maiqingqiang/typechat-go => ../../ 8 | -------------------------------------------------------------------------------- /examples/calendar/input.txt: -------------------------------------------------------------------------------- 1 | I need to get my tires changed from 12:00 to 2:00 pm on Friday March 15, 2024 2 | Search for any meetings with Gavin this week 3 | Set up an event for friday named Jeffs pizza party at 6pm 4 | Please add Jennifer to the scrum next Thursday 5 | Will you please add an appointment with Jerri Skinner at 9 am? I need it to last 2 hours 6 | Do I have any plan with Rosy this month? 7 | I need to add a meeting with my boss on Monday at 10am. Also make sure to schedule and appointment with Sally, May, and Boris tomorrow at 3pm. Now just add to it Jesse and Abby and make it last ninety minutes 8 | Add meeting with team today at 2 9 | can you record lunch with Luis at 12pm on Friday and also add Isobel to the Wednesday ping pong game at 4pm 10 | I said I'd meet with Jenny this afternoon at 2pm and after that I need to go to the dry cleaner and then the soccer game. Leave an hour for each of those starting at 3:30 11 | -------------------------------------------------------------------------------- /examples/calendar/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/maiqingqiang/typechat-go" 5 | "log" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | model, err := typechat.NewLanguageModel() 11 | if err != nil { 12 | log.Fatal(err) 13 | } 14 | 15 | schema, err := os.ReadFile("calendar_actions_schema.go") 16 | if err != nil { 17 | log.Fatalf("os.ReadFile Error: %v\n", err) 18 | } 19 | 20 | translator := typechat.NewJsonTranslator[CalendarActions](model, string(schema), "CalendarActions") 21 | 22 | _ = typechat.ProcessRequests("📅> ", os.Args[1], func(request string) error { 23 | calendarActions, err := translator.Translate(request) 24 | if err != nil { 25 | log.Fatalf("translator.Translate Error: %v\n", err) 26 | } 27 | 28 | log.Printf("%+v", calendarActions) 29 | 30 | for i := range calendarActions.Actions { 31 | if calendarActions.Actions[i].UnknownAction != nil { 32 | log.Fatalf("I didn't understand the following:\n%s", calendarActions.Actions[i].UnknownAction.Text) 33 | } 34 | } 35 | 36 | return nil 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /examples/coffee_shop/README.md: -------------------------------------------------------------------------------- 1 | # todo -------------------------------------------------------------------------------- /examples/math/README.md: -------------------------------------------------------------------------------- 1 | # Run the math example 2 | 3 | To run an example with one of these input files, run `go run . `. 4 | 5 | you can run: 6 | ``` 7 | go run . /input.txt 8 | ``` 9 | 10 | ![run.png](./run.png) -------------------------------------------------------------------------------- /examples/math/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/maiqingqiang/examples/math 2 | 3 | go 1.20 4 | 5 | require github.com/maiqingqiang/typechat-go v1.0.0 6 | 7 | require github.com/spf13/cast v1.5.1 // indirect 8 | 9 | replace github.com/maiqingqiang/typechat-go => ../../ 10 | -------------------------------------------------------------------------------- /examples/math/go.sum: -------------------------------------------------------------------------------- 1 | github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= 2 | github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= 3 | -------------------------------------------------------------------------------- /examples/math/input.txt: -------------------------------------------------------------------------------- 1 | 1 + 2 2 | 1 + 2 * 3 3 | 2 * 3 + 4 * 5 4 | 2 3 * 4 5 * + 5 | multiply two by three, then multiply four by five, then sum the results 6 | -------------------------------------------------------------------------------- /examples/math/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/maiqingqiang/typechat-go" 6 | "github.com/spf13/cast" 7 | "log" 8 | "os" 9 | ) 10 | 11 | func main() { 12 | model, err := typechat.NewLanguageModel() 13 | if err != nil { 14 | log.Fatal(err) 15 | } 16 | 17 | schema, err := os.ReadFile("math_schema.go") 18 | if err != nil { 19 | log.Fatalf("os.ReadFile Error: %v\n", err) 20 | } 21 | 22 | translator := typechat.NewProgramTranslator(model, string(schema)) 23 | 24 | _ = typechat.ProcessRequests("➕➖✖️➗🟰> ", os.Args[1], func(request string) error { 25 | response, err := translator.Translate(request) 26 | if err != nil { 27 | log.Fatalf("translator.Translate Error: %v\n", err) 28 | } 29 | 30 | programStr, err := translator.Validator().CreateModuleTextFromJson(response) 31 | if err != nil { 32 | log.Fatalf("CreateModuleTextFromJson Error: %v\n", err) 33 | } 34 | log.Println(programStr) 35 | 36 | log.Println(fmt.Sprintf("Running program:")) 37 | result, err := typechat.EvaluateJsonProgram(response, handleCall) 38 | if err != nil { 39 | log.Fatalf("EvaluateJsonProgram Error: %v\n", err) 40 | } 41 | log.Println(fmt.Sprintf("Result: %d", result)) 42 | 43 | return nil 44 | }) 45 | 46 | } 47 | 48 | func handleCall(fn string, args []typechat.Expression) (typechat.Result, error) { 49 | switch fn { 50 | case "Add": 51 | return cast.ToInt(args[0]) + cast.ToInt(args[1]), nil 52 | case "Sub": 53 | return cast.ToInt(args[0]) - cast.ToInt(args[1]), nil 54 | case "Mul": 55 | return cast.ToInt(args[0]) * cast.ToInt(args[1]), nil 56 | case "Div": 57 | return cast.ToInt(args[0]) / cast.ToInt(args[1]), nil 58 | case "Neg": 59 | return -cast.ToInt(args[0]), nil 60 | case "Id": 61 | return cast.ToInt(args[0]), nil 62 | } 63 | 64 | return 0, nil 65 | } 66 | -------------------------------------------------------------------------------- /examples/math/math_schema.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type API interface { 4 | // Add two numbers 5 | Add(x, y int) int 6 | // Sub Subtract two numbers 7 | Sub(x, y int) int 8 | // Mul Multiply two numbers 9 | Mul(x, y int) int 10 | // Div Divide two numbers 11 | Div(x, y int) int 12 | // Neg Negate a number 13 | Neg(x int) int 14 | // ID Identity function 15 | ID(x int) int 16 | // Unknown request 17 | Unknown(text string) int 18 | } 19 | -------------------------------------------------------------------------------- /examples/math/run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnmai-dev/TypeChat-Go/4d6432bcda926ad71caaf9e8fa41d9c123f40185/examples/math/run.png -------------------------------------------------------------------------------- /examples/music/README.md: -------------------------------------------------------------------------------- 1 | # todo -------------------------------------------------------------------------------- /examples/restaurant/README.md: -------------------------------------------------------------------------------- 1 | # Run the restaurant example 2 | 3 | To run an example with one of these input files, run `go run . `. 4 | 5 | you can run: 6 | ``` 7 | go run . /input.txt 8 | ``` 9 | 10 | ![run.png](./run.png) -------------------------------------------------------------------------------- /examples/restaurant/food_order_view_schema.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Order an order from a restaurant that serves pizza, beer, and salad 4 | type Order struct { 5 | Items []*OrderItem 6 | } 7 | 8 | type OrderItem struct { 9 | Pizza *Pizza `json:"pizza,omitempty"` 10 | Beer *Beer `json:"beer,omitempty"` 11 | Salad *Salad `json:"salad,omitempty"` 12 | NamedPizza *NamedPizza `json:"named_pizza,omitempty"` 13 | Unknown *UnknownText `json:"unknown,omitempty"` 14 | } 15 | 16 | // UnknownText Use this struct for order items that match nothing else 17 | type UnknownText struct { 18 | Text string // The text that wasn't understood 19 | } 20 | 21 | // SizeEnum Define a custom type SizeEnum with an underlying type of int. 22 | type SizeEnum int 23 | 24 | const ( 25 | UnknownSize SizeEnum = 0 26 | Small SizeEnum = 1 27 | Medium SizeEnum = 2 28 | Large SizeEnum = 3 29 | ExtraLarge SizeEnum = 4 30 | ) 31 | 32 | func (s SizeEnum) String() string { 33 | switch s { 34 | case Small: 35 | return "small" 36 | case Medium: 37 | return "medium" 38 | case Large: 39 | return "large" 40 | case ExtraLarge: 41 | return "extra large" 42 | default: 43 | return "" 44 | } 45 | } 46 | 47 | // NameEnum Define a custom type NameEnum with an underlying type of int. 48 | type NameEnum int 49 | 50 | const ( 51 | UnknownName NameEnum = iota 52 | Hawaiian 53 | Yeti 54 | PigInaForest 55 | CherryBomb 56 | ) 57 | 58 | func (n NameEnum) String() string { 59 | switch n { 60 | case Hawaiian: 61 | return "Hawaiian" 62 | case Yeti: 63 | return "Yeti" 64 | case PigInaForest: 65 | return "Pig In a Forest" 66 | case CherryBomb: 67 | return "Cherry Bomb" 68 | default: 69 | return "" 70 | } 71 | } 72 | 73 | type Pizza struct { 74 | Size SizeEnum `json:"size,omitempty"` // size use a custom SizeEnum type size with an underlying type of int, default: 3 75 | AddedToppings []string `json:"added_toppings,omitempty"` // toppings requested (examples: pepperoni, arugula) 76 | RemovedToppings []string `json:"removed_toppings,omitempty"` // toppings requested to be removed (examples: fresh garlic, anchovies) 77 | Quantity int `json:"quantity,omitempty"` // quantity, default: 1 78 | Name NameEnum `json:"name,omitempty"` // used if the requester references a pizza by name 79 | } 80 | 81 | type NamedPizza struct { 82 | Pizza 83 | } 84 | 85 | type Beer struct { 86 | Kind string // examples: Mack and Jacks, Sierra Nevada, Pale Ale, Miller Lite 87 | Quantity int `json:"quantity,omitempty"` // quantity, default: 1 88 | } 89 | 90 | var saladSize = []string{"half", "whole"} 91 | 92 | var saladStyle = []string{"Garden", "Greek"} 93 | 94 | type Salad struct { 95 | Portion string `json:"portion,omitempty"` // default: half 96 | Style string `json:"style,omitempty"` // default: Garden 97 | AddedIngredients []string `json:"added_ingredients,omitempty"` // ingredients requested (examples: parmesan, croutons) 98 | RemovedIngredients []string `json:"removed_ingredients,omitempty"` // ingredients requested to be removed (example: red onions) 99 | Quantity int `json:"quantity,omitempty"` // quantity, default: 1 100 | } 101 | -------------------------------------------------------------------------------- /examples/restaurant/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/maiqingqiang/examples/restaurant 2 | 3 | go 1.20 4 | 5 | require github.com/maiqingqiang/typechat-go v1.0.0 6 | 7 | replace github.com/maiqingqiang/typechat-go => ../../ -------------------------------------------------------------------------------- /examples/restaurant/input.txt: -------------------------------------------------------------------------------- 1 | I'd like two large, one with pepperoni and the other with extra sauce. The pepperoni gets basil and the extra sauce gets Canadian bacon. And add a whole salad. Make the Canadian bacon a medium. Make the salad a Greek with no red onions. And give me two Mack and Jacks and a Sierra Nevada. Oh, and add another salad with no red onions. 2 | I'd like two large with olives and mushrooms. And the first one gets extra sauce. The second one gets basil. Both get arugula. And add a Pale Ale. Give me a two Greeks with no red onions, a half and a whole. And a large with sausage and mushrooms. Plus three Pale Ales and a Mack and Jacks. 3 | I'll take two large with pepperoni. Put olives on one of them. Make the olive a small. And give me whole Greek plus a Pale Ale and an M&J. 4 | I want three pizzas, one with mushrooms and the other two with sausage. Make one sausage a small. And give me a whole Greek and a Pale Ale. And give me a Mack and Jacks. 5 | I would like to order one with basil and one with extra sauce. Throw in a salad and an ale. 6 | I would love to have a pepperoni with extra sauce, basil and arugula. Lovely weather we're having. Throw in some pineapple. And give me a whole Greek and a Pale Ale. Boy, those Mariners are doggin it. And how about a Mack and Jacks. 7 | I'll have two pepperoni, the first with extra sauce and the second with basil. Add pineapple to the first and add olives to the second. 8 | I sure am hungry for a pizza with pepperoni and a salad with no croutons. And I'm thirsty for 3 Pale Ales 9 | give me three regular salads and two Greeks and make the regular ones with no red onions 10 | I'll take four large pepperoni pizzas. Put extra sauce on two of them. plus an M&J and a Pale Ale 11 | I'll take a yeti, a pale ale and a large with olives and take the extra cheese off the yeti and add a Greek 12 | I'll take a medium Pig with no arugula 13 | I'll take a small Pig with no arugula and a Greek with croutons and no red onions 14 | 15 | -------------------------------------------------------------------------------- /examples/restaurant/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/maiqingqiang/typechat-go" 6 | "log" 7 | "os" 8 | ) 9 | 10 | var saladIngredients = []string{ 11 | "lettuce", 12 | "tomatoes", 13 | "red onions", 14 | "olives", 15 | "peppers", 16 | "parmesan", 17 | "croutons", 18 | } 19 | 20 | var pizzaToppings = []string{ 21 | "pepperoni", 22 | "sausage", 23 | "mushrooms", 24 | "basil", 25 | "extra cheese", 26 | "extra sauce", 27 | "anchovies", 28 | "pineapple", 29 | "olives", 30 | "arugula", 31 | "Canadian bacon", 32 | "Mama Lil's Peppers", 33 | } 34 | 35 | var namedPizzas = map[string][]string{ 36 | "Hawaiian": {"pineapple", "Canadian bacon"}, 37 | "Yeti": {"extra cheese", "extra sauce"}, 38 | "Pig In a Forest": {"mushrooms", "basil", "Canadian bacon", "arugula"}, 39 | "Cherry Bomb": {"pepperoni", "sausage", "Mama Lil's Peppers"}, 40 | } 41 | 42 | func main() { 43 | model, err := typechat.NewLanguageModel() 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | 48 | schema, err := os.ReadFile("food_order_view_schema.go") 49 | if err != nil { 50 | log.Fatalf("os.ReadFile Error: %v\n", err) 51 | } 52 | 53 | translator := typechat.NewJsonTranslator[Order](model, string(schema), "Order") 54 | 55 | _ = typechat.ProcessRequests("🍕> ", os.Args[1], func(request string) error { 56 | order, err := translator.Translate(request) 57 | if err != nil { 58 | log.Fatalf("translator.Translate Error: %v\n", err) 59 | } 60 | 61 | printOrder(order) 62 | 63 | return nil 64 | }) 65 | } 66 | 67 | func printOrder(order *Order) { 68 | if order != nil && len(order.Items) > 0 { 69 | for _, item := range order.Items { 70 | if item.Unknown != nil { 71 | break 72 | } 73 | 74 | if item.Pizza != nil || item.NamedPizza != nil { 75 | if item.Pizza.Name.String() != "" { 76 | addedToppings, ok := namedPizzas[item.Pizza.Name.String()] 77 | if ok { 78 | if item.Pizza.AddedToppings != nil { 79 | item.Pizza.AddedToppings = append(item.Pizza.AddedToppings, addedToppings...) 80 | } else { 81 | item.Pizza.AddedToppings = addedToppings 82 | } 83 | } 84 | } 85 | 86 | if item.Pizza.Size.String() == "" { 87 | item.Pizza.Size = Large 88 | } 89 | 90 | quantity := 1 91 | if item.Pizza.Quantity > 0 { 92 | quantity = item.Pizza.Quantity 93 | } 94 | 95 | pizzaStr := fmt.Sprintf(` %d %s pizza`, quantity, item.Pizza.Size.String()) 96 | 97 | if len(item.Pizza.AddedToppings) > 0 && len(item.Pizza.RemovedToppings) > 0 { 98 | item.Pizza.AddedToppings, item.Pizza.RemovedToppings = removeCommonStrings(item.Pizza.AddedToppings, item.Pizza.RemovedToppings) 99 | } 100 | 101 | if len(item.Pizza.AddedToppings) > 0 { 102 | pizzaStr += " with" 103 | for index, addedTopping := range item.Pizza.AddedToppings { 104 | if contains(pizzaToppings, addedTopping) { 105 | if index == 0 { 106 | pizzaStr += fmt.Sprintf(" %s", addedTopping) 107 | } else { 108 | pizzaStr += fmt.Sprintf(", %s", addedTopping) 109 | } 110 | } else { 111 | log.Printf("We are out of %s", addedTopping) 112 | } 113 | } 114 | } 115 | 116 | if len(item.Pizza.RemovedToppings) > 0 { 117 | pizzaStr += " and without" 118 | for index, removedTopping := range item.Pizza.RemovedToppings { 119 | if index == 0 { 120 | pizzaStr += fmt.Sprintf(" %s", removedTopping) 121 | } else { 122 | pizzaStr += fmt.Sprintf(", %s", removedTopping) 123 | } 124 | } 125 | } 126 | 127 | log.Printf(pizzaStr) 128 | } else if item.Beer != nil { 129 | quantity := 1 130 | if item.Beer.Quantity > 0 { 131 | quantity = item.Beer.Quantity 132 | } 133 | 134 | beerStr := fmt.Sprintf(" %d %s", quantity, item.Beer.Kind) 135 | log.Printf(beerStr) 136 | } else if item.Salad != nil { 137 | quantity := 1 138 | if item.Salad.Quantity > 0 { 139 | quantity = item.Salad.Quantity 140 | } 141 | 142 | if item.Salad.Portion == "" { 143 | item.Salad.Portion = "half" 144 | } 145 | 146 | if item.Salad.Style == "" { 147 | item.Salad.Style = "Garden" 148 | } 149 | 150 | saladStr := fmt.Sprintf(` %d %s %s salad`, quantity, item.Salad.Portion, item.Salad.Style) 151 | 152 | if len(item.Salad.AddedIngredients) > 0 && len(item.Salad.RemovedIngredients) > 0 { 153 | item.Salad.AddedIngredients, item.Salad.RemovedIngredients = removeCommonStrings(item.Salad.AddedIngredients, item.Salad.RemovedIngredients) 154 | } 155 | 156 | if len(item.Salad.AddedIngredients) > 0 { 157 | saladStr += " with" 158 | for index, addedIngredient := range item.Salad.AddedIngredients { 159 | if contains(saladIngredients, addedIngredient) { 160 | if index == 0 { 161 | saladStr += fmt.Sprintf(" %s", addedIngredient) 162 | } else { 163 | saladStr += fmt.Sprintf(", %s", addedIngredient) 164 | } 165 | } else { 166 | log.Printf("We are out of %s", addedIngredient) 167 | } 168 | } 169 | } 170 | 171 | if len(item.Salad.RemovedIngredients) > 0 { 172 | saladStr += " and without" 173 | for index, removedIngredient := range item.Salad.RemovedIngredients { 174 | if index == 0 { 175 | saladStr += fmt.Sprintf(" %s", removedIngredient) 176 | } else { 177 | saladStr += fmt.Sprintf(", %s", removedIngredient) 178 | } 179 | } 180 | } 181 | 182 | log.Printf(saladStr) 183 | } 184 | } 185 | } 186 | } 187 | 188 | func contains(arr []string, str string) bool { 189 | for _, a := range arr { 190 | if a == str { 191 | return true 192 | } 193 | } 194 | return false 195 | } 196 | 197 | func removeCommonStrings(a, b []string) ([]string, []string) { 198 | aSet := make(map[string]struct{}) 199 | for _, item := range a { 200 | aSet[item] = struct{}{} 201 | } 202 | 203 | bSet := make(map[string]struct{}) 204 | for _, item := range b { 205 | bSet[item] = struct{}{} 206 | } 207 | 208 | for item := range aSet { 209 | if _, ok := bSet[item]; ok { 210 | delete(aSet, item) 211 | delete(bSet, item) 212 | } 213 | } 214 | 215 | var aResult []string 216 | for item := range aSet { 217 | aResult = append(aResult, item) 218 | } 219 | 220 | var bResult []string 221 | for item := range bSet { 222 | bResult = append(bResult, item) 223 | } 224 | 225 | return aResult, bResult 226 | } 227 | -------------------------------------------------------------------------------- /examples/restaurant/run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnmai-dev/TypeChat-Go/4d6432bcda926ad71caaf9e8fa41d9c123f40185/examples/restaurant/run.png -------------------------------------------------------------------------------- /examples/sentiment/README.md: -------------------------------------------------------------------------------- 1 | # Run the sentiment example 2 | 3 | To run an example with one of these input files, run `go run . `. 4 | 5 | you can run: 6 | ``` 7 | go run . /input.txt 8 | ``` 9 | 10 | ![run.png](./run.png) -------------------------------------------------------------------------------- /examples/sentiment/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/maiqingqiang/examples/sentiment 2 | 3 | go 1.20 4 | 5 | require github.com/maiqingqiang/typechat-go v1.0.0 6 | 7 | replace github.com/maiqingqiang/typechat-go => ../../ 8 | -------------------------------------------------------------------------------- /examples/sentiment/go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnmai-dev/TypeChat-Go/4d6432bcda926ad71caaf9e8fa41d9c123f40185/examples/sentiment/go.sum -------------------------------------------------------------------------------- /examples/sentiment/input.txt: -------------------------------------------------------------------------------- 1 | hello, world 2 | TypeChat is awesome! 3 | I'm having a good day 4 | it's very rainy outside 5 | -------------------------------------------------------------------------------- /examples/sentiment/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/maiqingqiang/typechat-go" 5 | "log" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | model, err := typechat.NewLanguageModel() 11 | if err != nil { 12 | log.Fatal(err) 13 | } 14 | 15 | schema, err := os.ReadFile("sentiment_schema.go") 16 | if err != nil { 17 | log.Fatalf("os.ReadFile Error: %v\n", err) 18 | } 19 | 20 | translator := typechat.NewJsonTranslator[SentimentResponse](model, string(schema), "SentimentResponse") 21 | 22 | _ = typechat.ProcessRequests("😀> ", os.Args[1], func(request string) error { 23 | response, err := translator.Translate(request) 24 | if err != nil { 25 | log.Fatalf("translator.Translate Error: %v\n", err) 26 | } 27 | 28 | log.Printf("The sentiment is %s\n", Sentiment(response.Sentiment)) 29 | return nil 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /examples/sentiment/run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnmai-dev/TypeChat-Go/4d6432bcda926ad71caaf9e8fa41d9c123f40185/examples/sentiment/run.png -------------------------------------------------------------------------------- /examples/sentiment/sentiment_schema.go: -------------------------------------------------------------------------------- 1 | // The following is a schema definition for determining the sentiment of a some user input. 2 | 3 | package main 4 | 5 | import "fmt" 6 | 7 | // Sentiment Define the enum type, this value is int 8 | type Sentiment int 9 | 10 | // Define the enum constants for Sentiment 11 | const ( 12 | Negative Sentiment = iota 13 | Neutral 14 | Positive 15 | ) 16 | 17 | // Use switch statement to handle the enum type for Sentiment 18 | func (s Sentiment) String() string { 19 | switch s { 20 | case Negative: 21 | return "negative" 22 | case Neutral: 23 | return "neutral" 24 | case Positive: 25 | return "positive" 26 | default: 27 | return "" 28 | } 29 | } 30 | 31 | type SentimentResponse struct { 32 | Sentiment Sentiment `json:"sentiment"` // The sentiment of the Sentiment enum type 33 | } 34 | 35 | func (s SentimentResponse) String() string { 36 | return fmt.Sprintf("%s", s.Sentiment) 37 | } 38 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/maiqingqiang/typechat-go 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /interactive.go: -------------------------------------------------------------------------------- 1 | package typechat 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func ProcessRequests(interactivePrompt string, inputFileName string, processRequest func(request string) error) error { 10 | file, err := os.Open(inputFileName) 11 | if err != nil { 12 | return err 13 | } 14 | defer file.Close() 15 | 16 | scanner := bufio.NewScanner(file) 17 | for scanner.Scan() { 18 | line := scanner.Text() 19 | fmt.Printf("%s%s\n", interactivePrompt, line) 20 | err = processRequest(line) 21 | if err != nil { 22 | return err 23 | } 24 | } 25 | 26 | return scanner.Err() 27 | } 28 | -------------------------------------------------------------------------------- /model.go: -------------------------------------------------------------------------------- 1 | package typechat 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "os" 11 | "time" 12 | ) 13 | 14 | type response struct { 15 | Id string `json:"id"` 16 | Object string `json:"object"` 17 | Created int `json:"created"` 18 | Choices []*choice `json:"choices"` 19 | Usage *usage `json:"usage"` 20 | } 21 | 22 | type message struct { 23 | Role string `json:"role"` 24 | Content string `json:"content"` 25 | } 26 | 27 | type choice struct { 28 | Index int `json:"index"` 29 | Message *message `json:"message"` 30 | FinishReason string `json:"finish_reason"` 31 | } 32 | 33 | type usage struct { 34 | PromptTokens int `json:"prompt_tokens"` 35 | CompletionTokens int `json:"completion_tokens"` 36 | TotalTokens int `json:"total_tokens"` 37 | } 38 | 39 | type LanguageModel interface { 40 | complete(prompt string) (string, error) 41 | } 42 | 43 | func NewLanguageModel() (LanguageModel, error) { 44 | if os.Getenv("OPENAI_API_KEY") != "" { 45 | apiKey := os.Getenv("OPENAI_API_KEY") 46 | 47 | model := os.Getenv("OPENAI_MODEL") 48 | if model == "" { 49 | return nil, missingEnvironmentVariable("OPENAI_MODEL") 50 | } 51 | 52 | endPoint := os.Getenv("OPENAI_ENDPOINT") 53 | if endPoint == "" { 54 | endPoint = "https://api.openai.com/v1/chat/completions" 55 | } 56 | 57 | return NewOpenAILanguageModel(apiKey, model, endPoint, os.Getenv("OPENAI_ORGANIZATION")), nil 58 | } 59 | 60 | if os.Getenv("AZURE_OPENAI_API_KEY") != "" { 61 | apiKey := os.Getenv("AZURE_OPENAI_API_KEY") 62 | endPoint := os.Getenv("AZURE_OPENAI_ENDPOINT") 63 | if endPoint == "" { 64 | return nil, missingEnvironmentVariable("AZURE_OPENAI_ENDPOINT") 65 | } 66 | 67 | return NewAzureOpenAILanguageModel(apiKey, endPoint), nil 68 | } 69 | 70 | return nil, missingEnvironmentVariable("OPENAI_API_KEY or AZURE_OPENAI_API_KEY") 71 | } 72 | 73 | type Option func(*baseLanguageModel) 74 | 75 | func WithHeaders(headers map[string]string) func(*baseLanguageModel) { 76 | return func(m *baseLanguageModel) { 77 | m.headers = headers 78 | } 79 | } 80 | 81 | func WithDefaultParams(defaultParams map[string]any) func(*baseLanguageModel) { 82 | return func(m *baseLanguageModel) { 83 | m.defaultParams = defaultParams 84 | } 85 | } 86 | 87 | type baseLanguageModel struct { 88 | url string 89 | retryMaxAttempts int 90 | retryPauseDuration time.Duration 91 | headers map[string]string 92 | defaultParams map[string]any 93 | } 94 | 95 | func newBaseLanguageModel(url string, options ...Option) LanguageModel { 96 | m := &baseLanguageModel{ 97 | url: url, 98 | retryMaxAttempts: 3, 99 | retryPauseDuration: 1000 * time.Millisecond, 100 | headers: make(map[string]string), 101 | } 102 | 103 | for _, option := range options { 104 | option(m) 105 | } 106 | 107 | return m 108 | } 109 | 110 | func (m *baseLanguageModel) complete(prompt string) (string, error) { 111 | retryCount := 0 112 | 113 | for { 114 | paramMap := map[string]any{ 115 | "messages": []map[string]any{ 116 | { 117 | "role": "user", 118 | "content": prompt, 119 | }, 120 | }, 121 | "temperature": 0, 122 | "n": 1, 123 | } 124 | 125 | if m.defaultParams != nil { 126 | for k, v := range m.defaultParams { 127 | paramMap[k] = v 128 | } 129 | } 130 | 131 | params, err := json.Marshal(paramMap) 132 | 133 | if err != nil { 134 | return "", err 135 | } 136 | 137 | req, err := http.NewRequest(http.MethodPost, m.url, bytes.NewBuffer(params)) 138 | if err != nil { 139 | return "", err 140 | } 141 | 142 | req.Header.Add("Content-Type", "application/json;charset=utf-8") 143 | 144 | if m.headers != nil { 145 | for k, v := range m.headers { 146 | req.Header.Add(k, v) 147 | } 148 | } 149 | 150 | client := http.Client{} 151 | 152 | resp, err := client.Do(req) 153 | if err != nil { 154 | return "", err 155 | } 156 | 157 | bodyBytes, err := m.readBody(resp) 158 | if err != nil { 159 | return "", err 160 | } 161 | 162 | if resp.StatusCode == http.StatusOK { 163 | 164 | var r response 165 | err = json.Unmarshal(bodyBytes, &r) 166 | if err != nil { 167 | return "", err 168 | } 169 | 170 | if len(r.Choices) == 0 || r.Choices[0].Message == nil { 171 | return "", nil 172 | } 173 | 174 | return r.Choices[0].Message.Content, nil 175 | } 176 | 177 | if !m.isTransientHttpError(resp.StatusCode) || retryCount >= m.retryMaxAttempts { 178 | return "", errors.New(fmt.Sprintf("REST API error %d: %s", resp.StatusCode, resp.Status)) 179 | } 180 | 181 | time.Sleep(m.retryPauseDuration) 182 | retryCount++ 183 | } 184 | } 185 | 186 | func (m *baseLanguageModel) readBody(resp *http.Response) ([]byte, error) { 187 | defer resp.Body.Close() 188 | respBytes, err := io.ReadAll(resp.Body) 189 | if err != nil { 190 | return nil, err 191 | } 192 | 193 | return respBytes, nil 194 | } 195 | 196 | // 429: TooManyRequests 197 | // 500: InternalServerError 198 | // 502: BadGateway 199 | // 503: ServiceUnavailable 200 | // 504: GatewayTimeout 201 | func (m *baseLanguageModel) isTransientHttpError(code int) bool { 202 | return code == 429 || code == 500 || code == 502 || code == 503 || code == 504 203 | } 204 | 205 | // NewAzureOpenAILanguageModel Creates a language model encapsulation of an Azure OpenAI REST API endpoint. 206 | func NewAzureOpenAILanguageModel(apiKey, endPoint string) LanguageModel { 207 | return newBaseLanguageModel(endPoint, WithHeaders(map[string]string{ 208 | "api-key": apiKey, 209 | })) 210 | } 211 | 212 | // NewOpenAILanguageModel Creates a language model encapsulation of an OpenAI REST API endpoint. 213 | func NewOpenAILanguageModel(apiKey, model, endPoint, org string) LanguageModel { 214 | return newBaseLanguageModel( 215 | endPoint, 216 | WithHeaders(map[string]string{ 217 | "Authorization": fmt.Sprintf("Bearer %s", apiKey), 218 | "OpenAI-Organization": org, 219 | }), 220 | WithDefaultParams(map[string]any{ 221 | "model": model, 222 | }), 223 | ) 224 | } 225 | 226 | func missingEnvironmentVariable(name string) error { 227 | return fmt.Errorf("Missing environment variable: %s", name) 228 | } 229 | -------------------------------------------------------------------------------- /program.go: -------------------------------------------------------------------------------- 1 | package typechat 2 | 3 | import ( 4 | _ "embed" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | var ( 11 | //go:embed program_schema.tpl 12 | programSchemaText string 13 | ) 14 | 15 | const Steps = "@step" 16 | const Func = "@func" 17 | const Args = "@args" 18 | const Ref = "@ref" 19 | 20 | type Program struct { 21 | Steps []*FuncCall `json:"@steps"` 22 | } 23 | 24 | type FuncCall struct { 25 | Func string `json:"@func"` 26 | Args []Expression `json:"@args,omitempty"` 27 | } 28 | 29 | // Expression is a int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | uintptr | float32 | float64 | complex64 | complex128 | string | Program | ResultReference 30 | type Expression any 31 | type Result any 32 | 33 | type ResultReference struct { 34 | Ref int `json:"@ref"` 35 | } 36 | 37 | type ProgramTranslator struct { 38 | JsonTranslator[Program] 39 | } 40 | 41 | func NewProgramTranslator(model LanguageModel, schema string) JsonTranslator[Program] { 42 | return &ProgramTranslator{ 43 | &baseJsonTranslator[Program]{ 44 | model: model, 45 | validator: NewProgramValidator(schema), 46 | }, 47 | } 48 | } 49 | 50 | func (t *ProgramTranslator) CreateRequestPrompt(request string) string { 51 | return fmt.Sprintf("You are a service that translates user requests into programs represented as JSON using the following Go definitions:\n"+ 52 | "```\n%s```\n"+ 53 | "The programs can call functions from the API defined in the following Go definitions:\n"+ 54 | "```\n%s```\n"+ 55 | "The following is a user request:\n"+ 56 | "```\n%s\n```\n"+ 57 | "The following is the user request translated into a JSON program object with 2 spaces of indentation and no properties with the value undefined:\n", 58 | programSchemaText, t.Validator().GetSchema(), request) 59 | } 60 | 61 | func (t *ProgramTranslator) CreateRepairPrompt(validationError string) string { 62 | return fmt.Sprintf("The JSON program object is invalid for the following reason:\n"+ 63 | "\"\"\"\n%s\n\"\"\""+ 64 | "The following is a revised JSON program object:\n", 65 | validationError) 66 | } 67 | 68 | func (t *ProgramTranslator) Translate(request string) (*Program, error) { 69 | prompt := t.CreateRequestPrompt(request) 70 | 71 | resp, err := t.JsonTranslator.Model().complete(prompt) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | startIndex := strings.Index(resp, "{") 77 | endIndex := strings.LastIndex(resp, "}") 78 | 79 | if !(startIndex >= 0 && endIndex > startIndex) { 80 | return nil, errors.New(fmt.Sprintf("Response is not JSON:\n%s", resp)) 81 | } 82 | 83 | jsonText := resp[startIndex : endIndex+1] 84 | program, err := t.Validator().Validate(jsonText) 85 | if err == nil { 86 | return program, nil 87 | } 88 | 89 | prompt += fmt.Sprintf("%s\n%s", jsonText, t.CreateRepairPrompt(err.Error())) 90 | 91 | return nil, nil 92 | } 93 | 94 | func (t *ProgramTranslator) Validator() JsonValidator[Program] { 95 | return t.JsonTranslator.Validator() 96 | } 97 | 98 | type OnCallFunc func(fn string, args []Expression) (Result, error) 99 | 100 | func EvaluateJsonProgram(program *Program, onCall OnCallFunc) (Result, error) { 101 | var results []Result 102 | 103 | for _, step := range program.Steps { 104 | result, err := evaluate(step, onCall, results) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | results = append(results, result) 110 | } 111 | 112 | if len(results) > 0 { 113 | return results[len(results)-1], nil 114 | } 115 | return nil, nil 116 | } 117 | 118 | func evaluate(funcCall *FuncCall, onCall OnCallFunc, results []Result) (Result, error) { 119 | var expressions []Expression 120 | 121 | for i := range funcCall.Args { 122 | switch funcCall.Args[i].(type) { 123 | case map[string]any: 124 | m := funcCall.Args[i].(map[string]any) 125 | if _, ok := m[Func]; ok { 126 | result, err := onCall(m[Func].(string), evaluateArray(m[Args].([]any), onCall)) 127 | if err != nil { 128 | return nil, err 129 | } 130 | expressions = append(expressions, result) 131 | 132 | } else if _, ok := m[Ref]; ok { 133 | expressions = append(expressions, results[int(m[Ref].(float64))]) 134 | } 135 | case int: 136 | expressions = append(expressions, funcCall.Args[i]) 137 | case float64: 138 | expressions = append(expressions, funcCall.Args[i]) 139 | } 140 | } 141 | 142 | result, err := onCall(funcCall.Func, expressions) 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | return result, nil 148 | } 149 | 150 | func evaluateArray(args []any, onCall OnCallFunc) []Expression { 151 | var expressions []Expression 152 | for _, arg := range args { 153 | switch arg.(type) { 154 | case map[string]any: 155 | m := arg.(map[string]any) 156 | if _, ok := m[Func]; ok { 157 | result, err := onCall(m[Func].(string), evaluateArray(m[Args].([]any), onCall)) 158 | if err != nil { 159 | return nil 160 | } 161 | expressions = append(expressions, result) 162 | } 163 | case int: 164 | expressions = append(expressions, arg) 165 | case float64: 166 | expressions = append(expressions, arg) 167 | } 168 | } 169 | 170 | return expressions 171 | } 172 | -------------------------------------------------------------------------------- /program_schema.tpl: -------------------------------------------------------------------------------- 1 | // A program consists of a sequence of function calls that are evaluated in order. 2 | type Program struct { 3 | Steps []FuncCall `json:"@steps"` 4 | } 5 | 6 | // A function call specifies a function name and a list of argument expressions. Arguments may contain 7 | // nested function calls and result references. 8 | type FuncCall struct { 9 | // Name of the function 10 | Func string `json:"@func"` 11 | // Arguments for the function, if any 12 | Args []Expression `json:"@args,omitempty"` 13 | } 14 | 15 | // An expression is a int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | uintptr | float32 | float64 | complex64 | complex128 | string | FuncCall | ResultReference. 16 | type Expression any 17 | type Result any 18 | 19 | // A result reference represents the value of an expression from a preceding step. 20 | type ResultReference struct { 21 | // Index of the previous expression in the "@steps" array 22 | Ref int `json:"@ref"` 23 | } -------------------------------------------------------------------------------- /typechat.go: -------------------------------------------------------------------------------- 1 | package typechat 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type JsonTranslator[T any] interface { 9 | CreateRequestPrompt(request string) string 10 | CreateRepairPrompt(validationError string) string 11 | Translate(request string) (*T, error) 12 | Validator() JsonValidator[T] 13 | Model() LanguageModel 14 | } 15 | 16 | type baseJsonTranslator[T any] struct { 17 | model LanguageModel 18 | validator JsonValidator[T] 19 | attemptRepair bool 20 | stripNulls bool 21 | } 22 | 23 | func NewJsonTranslator[T any](model LanguageModel, schema string, typeName string) JsonTranslator[T] { 24 | return &baseJsonTranslator[T]{ 25 | model: model, 26 | validator: NewJsonValidator[T](schema, typeName), 27 | attemptRepair: true, 28 | } 29 | } 30 | 31 | func (t *baseJsonTranslator[T]) CreateRequestPrompt(request string) string { 32 | return fmt.Sprintf("You are a service that translates user requests into JSON objects of struct \"%s\" according to the following Go definitions:\n"+ 33 | "```go\n%s```\n"+ 34 | "The following is a user request:\n"+ 35 | "\"\"\"\n%s\n\"\"\"\n"+ 36 | "The following is the user request translated into a JSON object with 1 spaces of indentation and no properties with the value undefined:\n", 37 | t.validator.GetTypeName(), t.validator.GetSchema(), request) 38 | } 39 | 40 | func (t *baseJsonTranslator[T]) CreateRepairPrompt(validationError string) string { 41 | return fmt.Sprintf("The JSON object is invalid for the following reason:\n"+ 42 | "\"\"\"\n%s\n\"\"\"\n"+ 43 | "The following is a revised JSON object:\n", validationError) 44 | } 45 | 46 | func (t *baseJsonTranslator[T]) Translate(request string) (*T, error) { 47 | prompt := t.CreateRequestPrompt(request) 48 | attemptRepair := t.attemptRepair 49 | 50 | for { 51 | resp, err := t.model.complete(prompt) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | startIndex := strings.Index(resp, "{") 57 | endIndex := strings.LastIndex(resp, "}") 58 | 59 | if !(startIndex >= 0 && endIndex > startIndex) { 60 | return nil, fmt.Errorf("Response is not JSON:\n%s", resp) 61 | } 62 | 63 | jsonText := resp[startIndex : endIndex+1] 64 | 65 | result, err := t.validator.Validate(jsonText) 66 | 67 | if err == nil { 68 | return result, nil 69 | } 70 | 71 | if !attemptRepair { 72 | return nil, fmt.Errorf("JSON validation failed: %v\n%s", err, jsonText) 73 | } 74 | 75 | prompt += fmt.Sprintf("%s\n%s", jsonText, t.CreateRepairPrompt(err.Error())) 76 | attemptRepair = false 77 | } 78 | } 79 | 80 | func (t *baseJsonTranslator[T]) Validator() JsonValidator[T] { 81 | return t.validator 82 | } 83 | 84 | func (t *baseJsonTranslator[T]) Model() LanguageModel { 85 | return t.model 86 | } 87 | -------------------------------------------------------------------------------- /validate.go: -------------------------------------------------------------------------------- 1 | package typechat 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | type JsonValidator[T any] interface { 11 | GetSchema() string 12 | GetTypeName() string 13 | CreateModuleTextFromJson(jsonObject *T) (string, error) 14 | Validate(jsonText string) (*T, error) 15 | } 16 | 17 | type baseJsonValidator[T any] struct { 18 | schema string 19 | typeName string 20 | stripNulls bool 21 | } 22 | 23 | func (v *baseJsonValidator[T]) GetSchema() string { 24 | return v.schema 25 | } 26 | 27 | func (v *baseJsonValidator[T]) GetTypeName() string { 28 | return v.typeName 29 | } 30 | 31 | func (v *baseJsonValidator[T]) CreateModuleTextFromJson(jsonObject *T) (string, error) { 32 | marshal, err := json.Marshal(jsonObject) 33 | if err != nil { 34 | return "", err 35 | } 36 | 37 | return fmt.Sprintf("package main\n\nconst json = `%s`", marshal), nil 38 | } 39 | 40 | func (v *baseJsonValidator[T]) Validate(jsonText string) (*T, error) { 41 | var result *T 42 | err := json.Unmarshal([]byte(jsonText), &result) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return result, nil 48 | } 49 | 50 | func NewJsonValidator[T any](schema string, typeName string) JsonValidator[T] { 51 | return &baseJsonValidator[T]{ 52 | schema: schema, 53 | typeName: typeName, 54 | stripNulls: true, 55 | } 56 | } 57 | 58 | type ProgramValidator struct { 59 | JsonValidator[Program] 60 | } 61 | 62 | const ModuleText = `package main 63 | 64 | func program(api API) Result { 65 | %s 66 | } 67 | ` 68 | 69 | func NewProgramValidator(schema string) JsonValidator[Program] { 70 | return &ProgramValidator{ 71 | NewJsonValidator[Program](schema, "Program"), 72 | } 73 | } 74 | 75 | func (v *ProgramValidator) CreateModuleTextFromJson(program *Program) (string, error) { 76 | stepsLen := len(program.Steps) 77 | 78 | if !(stepsLen > 0 && program.Steps[0].Func != "") { 79 | return "", errors.New("struct is not a valid program") 80 | } 81 | 82 | currentStep := 0 83 | funcBody := "" 84 | for currentStep < stepsLen { 85 | if (stepsLen - 1) == currentStep { 86 | funcBody += fmt.Sprintf("return %s", v.exprToString(program.Steps[currentStep])) 87 | } else { 88 | funcBody += fmt.Sprintf("step%d := %s \n", currentStep+1, v.exprToString(program.Steps[currentStep])) 89 | } 90 | 91 | currentStep++ 92 | } 93 | 94 | return fmt.Sprintf(ModuleText, funcBody), nil 95 | } 96 | 97 | func (v *ProgramValidator) exprToString(expr *FuncCall) string { 98 | return v.objectToString(expr) 99 | } 100 | 101 | func (v *ProgramValidator) objectToString(expr *FuncCall) string { 102 | fn := expr.Func 103 | 104 | if len(expr.Args) > 0 { 105 | return fmt.Sprintf("api.%s(%s)", fn, v.arrayToString(expr.Args)) 106 | } else { 107 | return fmt.Sprintf("api.%s()", fn) 108 | } 109 | } 110 | 111 | func (v *ProgramValidator) arrayToString(args []Expression) string { 112 | var list []string 113 | for i := range args { 114 | switch args[i].(type) { 115 | case map[string]any: 116 | m := args[i].(map[string]any) 117 | 118 | var a []Expression 119 | 120 | if _, ok := m[Args]; ok { 121 | for _, item := range m[Args].([]any) { 122 | a = append(a, item) 123 | } 124 | 125 | list = append(list, v.objectToString(&FuncCall{ 126 | Func: m[Func].(string), 127 | Args: a, 128 | })) 129 | } else if _, ok := m[Ref]; ok { 130 | 131 | } 132 | case int: 133 | list = append(list, fmt.Sprintf("%v", args[i])) 134 | case float64: 135 | list = append(list, fmt.Sprintf("%v", args[i])) 136 | } 137 | } 138 | return strings.Join(list, ",") 139 | } 140 | --------------------------------------------------------------------------------