Skip to content

feat: output filters #532

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pkg/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ const (
CredentialToolCategory ToolCategory = "credential"
ContextToolCategory ToolCategory = "context"
InputToolCategory ToolCategory = "input"
OutputToolCategory ToolCategory = "output"
NoCategory ToolCategory = ""
)

Expand Down
5 changes: 5 additions & 0 deletions pkg/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ func isParam(line string, tool *types.Tool) (_ bool, err error) {
tool.Parameters.InputFilters = append(tool.Parameters.InputFilters, csv(value)...)
case "shareinputfilter", "shareinputfilters":
tool.Parameters.ExportInputFilters = append(tool.Parameters.ExportInputFilters, csv(value)...)
case "outputfilter", "outputfilters":
tool.Parameters.OutputFilters = append(tool.Parameters.OutputFilters, csv(value)...)
case "shareoutputfilter", "shareoutputfilters":
tool.Parameters.ExportOutputFilters = append(tool.Parameters.ExportOutputFilters, csv(value)...)
case "agent", "agents":
tool.Parameters.Agents = append(tool.Parameters.Agents, csv(value)...)
case "globaltool", "globaltools":
Expand Down Expand Up @@ -194,6 +198,7 @@ func (c *context) finish(tools *[]Node) {
c.tool.GlobalModelName != "" ||
len(c.tool.GlobalTools) > 0 ||
len(c.tool.ExportInputFilters) > 0 ||
len(c.tool.ExportOutputFilters) > 0 ||
c.tool.Chat {
*tools = append(*tools, Node{
ToolNode: &ToolNode{
Expand Down
24 changes: 24 additions & 0 deletions pkg/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,27 @@ share input filters: shared
}},
}}).Equal(t, out)
}

func TestParseOutput(t *testing.T) {
output := `
output filters: output
share output filters: shared
`
out, err := Parse(strings.NewReader(output))
require.NoError(t, err)
autogold.Expect(Document{Nodes: []Node{
{ToolNode: &ToolNode{
Tool: types.Tool{
ToolDef: types.ToolDef{
Parameters: types.Parameters{
OutputFilters: []string{
"output",
},
ExportOutputFilters: []string{"shared"},
},
},
Source: types.ToolSource{LineNo: 1},
},
}},
}}).Equal(t, out)
}
9 changes: 8 additions & 1 deletion pkg/runner/input.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package runner

import (
"encoding/json"
"fmt"

"github.com/gptscript-ai/gptscript/pkg/engine"
Expand All @@ -13,7 +14,13 @@ func (r *Runner) handleInput(callCtx engine.Context, monitor Monitor, env []stri
}

for _, inputToolRef := range inputToolRefs {
res, err := r.subCall(callCtx.Ctx, callCtx, monitor, env, inputToolRef.ToolID, input, "", engine.InputToolCategory)
inputData, err := json.Marshal(map[string]any{
"input": input,
})
if err != nil {
return "", fmt.Errorf("failed to marshal input: %w", err)
}
res, err := r.subCall(callCtx.Ctx, callCtx, monitor, env, inputToolRef.ToolID, string(inputData), "", engine.InputToolCategory)
if err != nil {
return "", err
}
Expand Down
72 changes: 72 additions & 0 deletions pkg/runner/output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package runner

import (
"encoding/json"
"errors"
"fmt"

"github.com/gptscript-ai/gptscript/pkg/engine"
)

func (r *Runner) handleOutput(callCtx engine.Context, monitor Monitor, env []string, state *State, retErr error) (*State, error) {
outputToolRefs, err := callCtx.Tool.GetOutputFilterTools(*callCtx.Program)
if err != nil {
return nil, err
}

if len(outputToolRefs) == 0 {
return state, retErr
}

var (
continuation bool
chatFinish bool
output string
)

if errMessage := (*engine.ErrChatFinish)(nil); errors.As(retErr, &errMessage) && callCtx.Tool.Chat {
chatFinish = true
output = errMessage.Message
} else if retErr != nil {
return state, retErr
} else if state.Continuation != nil && state.Continuation.Result != nil {
continuation = true
output = *state.Continuation.Result
} else if state.Result != nil {
output = *state.Result
} else {
return state, nil
}

for _, outputToolRef := range outputToolRefs {
inputData, err := json.Marshal(map[string]any{
"output": output,
"chatFinish": chatFinish,
"continuation": continuation,
"chat": callCtx.Tool.Chat,
})
if err != nil {
return nil, fmt.Errorf("marshaling input for output filter: %w", err)
}
res, err := r.subCall(callCtx.Ctx, callCtx, monitor, env, outputToolRef.ToolID, string(inputData), "", engine.OutputToolCategory)
if err != nil {
return nil, err
}
if res.Result == nil {
return nil, fmt.Errorf("invalid state: output tool [%s] can not result in a chat continuation", outputToolRef.Reference)
}
output = *res.Result
}

if chatFinish {
return state, &engine.ErrChatFinish{
Message: output,
}
} else if continuation {
state.Continuation.Result = &output
} else {
state.Result = &output
}

return state, nil
}
6 changes: 5 additions & 1 deletion pkg/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,11 @@ type Needed struct {
Input string `json:"input,omitempty"`
}

func (r *Runner) resume(callCtx engine.Context, monitor Monitor, env []string, state *State) (*State, error) {
func (r *Runner) resume(callCtx engine.Context, monitor Monitor, env []string, state *State) (retState *State, retErr error) {
defer func() {
retState, retErr = r.handleOutput(callCtx, monitor, env, retState, retErr)
}()

if state.StartContinuation {
return nil, fmt.Errorf("invalid state, resume should not have StartContinuation set to true")
}
Expand Down
46 changes: 46 additions & 0 deletions pkg/tests/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,52 @@ func TestInput(t *testing.T) {
autogold.ExpectFile(t, toJSONString(t, resp), autogold.Name(t.Name()+"/step2"))
}

func TestOutput(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip()
}

r := tester.NewRunner(t)
r.RespondWith(tester.Result{
Text: "Response 1",
})

prg, err := r.Load("")
require.NoError(t, err)

resp, err := r.Chat(context.Background(), nil, prg, nil, "Input 1")
require.NoError(t, err)
r.AssertResponded(t)
assert.False(t, resp.Done)
autogold.Expect(`CHAT: true CONTENT: Response 1 CONTINUATION: true FINISH: false suffix
`).Equal(t, resp.Content)
autogold.ExpectFile(t, toJSONString(t, resp), autogold.Name(t.Name()+"/step1"))

r.RespondWith(tester.Result{
Text: "Response 2",
})
resp, err = r.Chat(context.Background(), resp.State, prg, nil, "Input 2")
require.NoError(t, err)
r.AssertResponded(t)
assert.False(t, resp.Done)
autogold.Expect(`CHAT: true CONTENT: Response 2 CONTINUATION: true FINISH: false suffix
`).Equal(t, resp.Content)
autogold.ExpectFile(t, toJSONString(t, resp), autogold.Name(t.Name()+"/step2"))

r.RespondWith(tester.Result{
Err: &engine.ErrChatFinish{
Message: "Chat Done",
},
})
resp, err = r.Chat(context.Background(), resp.State, prg, nil, "Input 3")
require.NoError(t, err)
r.AssertResponded(t)
assert.True(t, resp.Done)
autogold.Expect(`CHAT FINISH: CHAT: true CONTENT: Chat Done CONTINUATION: false FINISH: true suffix
`).Equal(t, resp.Content)
autogold.ExpectFile(t, toJSONString(t, resp), autogold.Name(t.Name()+"/step3"))
}

func TestSysContext(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip()
Expand Down
6 changes: 4 additions & 2 deletions pkg/tests/testdata/TestInput/test.gpt
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ Tool body
---
name: taunt
args: foo: this is useless
args: input: this is used
#!/bin/bash

echo "No, ${GPTSCRIPT_INPUT}!"
echo "No, ${INPUT}!"

---
name: exporter
Expand All @@ -18,6 +19,7 @@ share input filters: taunt2
---
name: taunt2
args: foo: this is useless
args: input: this is used

#!/bin/bash
echo "${GPTSCRIPT_INPUT} ha ha ha"
echo "${INPUT} ha ha ha"
9 changes: 9 additions & 0 deletions pkg/tests/testdata/TestOutput/call1-resp.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
`{
"role": "assistant",
"content": [
{
"text": "Response 1"
}
],
"usage": {}
}`
24 changes: 24 additions & 0 deletions pkg/tests/testdata/TestOutput/call1.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
`{
"model": "gpt-4o",
"internalSystemPrompt": false,
"messages": [
{
"role": "system",
"content": [
{
"text": "\nTool body"
}
],
"usage": {}
},
{
"role": "user",
"content": [
{
"text": "Input 1"
}
],
"usage": {}
}
]
}`
9 changes: 9 additions & 0 deletions pkg/tests/testdata/TestOutput/call2-resp.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
`{
"role": "assistant",
"content": [
{
"text": "Response 2"
}
],
"usage": {}
}`
42 changes: 42 additions & 0 deletions pkg/tests/testdata/TestOutput/call2.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
`{
"model": "gpt-4o",
"internalSystemPrompt": false,
"messages": [
{
"role": "system",
"content": [
{
"text": "\nTool body"
}
],
"usage": {}
},
{
"role": "user",
"content": [
{
"text": "Input 1"
}
],
"usage": {}
},
{
"role": "assistant",
"content": [
{
"text": "Response 1"
}
],
"usage": {}
},
{
"role": "user",
"content": [
{
"text": "Input 2"
}
],
"usage": {}
}
]
}`
60 changes: 60 additions & 0 deletions pkg/tests/testdata/TestOutput/call3.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
`{
"model": "gpt-4o",
"internalSystemPrompt": false,
"messages": [
{
"role": "system",
"content": [
{
"text": "\nTool body"
}
],
"usage": {}
},
{
"role": "user",
"content": [
{
"text": "Input 1"
}
],
"usage": {}
},
{
"role": "assistant",
"content": [
{
"text": "Response 1"
}
],
"usage": {}
},
{
"role": "user",
"content": [
{
"text": "Input 2"
}
],
"usage": {}
},
{
"role": "assistant",
"content": [
{
"text": "Response 2"
}
],
"usage": {}
},
{
"role": "user",
"content": [
{
"text": "Input 3"
}
],
"usage": {}
}
]
}`
Loading
Loading