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