diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index b0a6e4eb..43c1da99 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -207,6 +207,11 @@ func NewContext(ctx context.Context, prg *types.Program, input string) (Context, } callCtx.AgentGroup = agentGroup + + if callCtx.Tool.IsAgentsOnly() && len(callCtx.AgentGroup) > 0 { + callCtx.Tool = callCtx.Program.ToolSet[callCtx.AgentGroup[0].ToolID] + } + return callCtx, nil } diff --git a/pkg/input/input.go b/pkg/input/input.go index 0037fa5e..3d480431 100644 --- a/pkg/input/input.go +++ b/pkg/input/input.go @@ -3,10 +3,12 @@ package input import ( "fmt" "io" + "io/fs" "os" "path/filepath" "strings" + "github.com/gptscript-ai/gptscript/internal" "github.com/gptscript-ai/gptscript/pkg/loader" "github.com/gptscript-ai/gptscript/pkg/types" ) @@ -33,7 +35,7 @@ func FromFile(file string) (string, error) { } return string(data), nil } else if file != "" { - if s, err := os.Stat(file); err == nil && s.IsDir() { + if s, err := fs.Stat(internal.FS, file); err == nil && s.IsDir() { for _, ext := range types.DefaultFiles { if _, err := os.Stat(filepath.Join(file, ext)); err == nil { file = filepath.Join(file, ext) @@ -42,7 +44,7 @@ func FromFile(file string) (string, error) { } } log.Debugf("reading file %s", file) - data, err := os.ReadFile(file) + data, err := fs.ReadFile(internal.FS, file) if err != nil { return "", fmt.Errorf("reading %s: %w", file, err) } diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go index b998320b..22cd5e9e 100644 --- a/pkg/parser/parser.go +++ b/pkg/parser/parser.go @@ -199,6 +199,7 @@ func (c *context) finish(tools *[]Node) { len(c.tool.GlobalTools) > 0 || len(c.tool.ExportInputFilters) > 0 || len(c.tool.ExportOutputFilters) > 0 || + len(c.tool.Agents) > 0 || c.tool.Chat { *tools = append(*tools, Node{ ToolNode: &ToolNode{ diff --git a/pkg/tests/runner_test.go b/pkg/tests/runner_test.go index 6efe4a11..0b75c8a2 100644 --- a/pkg/tests/runner_test.go +++ b/pkg/tests/runner_test.go @@ -800,6 +800,26 @@ func TestExport(t *testing.T) { assert.Equal(t, "TEST RESULT CALL: 3", x) } +func TestAgentOnly(t *testing.T) { + r := tester.NewRunner(t) + + prg, err := r.Load("") + require.NoError(t, err) + + r.RespondWith(tester.Result{ + Func: types.CompletionFunctionCall{ + Name: "agent2", + Arguments: "Agent 2 input", + }, + }) + + 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("TEST RESULT CALL: 2").Equal(t, resp.Content) + autogold.ExpectFile(t, toJSONString(t, resp), autogold.Name(t.Name()+"/step1")) +} func TestAgents(t *testing.T) { r := tester.NewRunner(t) diff --git a/pkg/tests/testdata/TestAgentOnly/call1-resp.golden b/pkg/tests/testdata/TestAgentOnly/call1-resp.golden new file mode 100644 index 00000000..ea865122 --- /dev/null +++ b/pkg/tests/testdata/TestAgentOnly/call1-resp.golden @@ -0,0 +1,16 @@ +`{ + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 0, + "id": "call_1", + "function": { + "name": "agent2", + "arguments": "Agent 2 input" + } + } + } + ], + "usage": {} +}` diff --git a/pkg/tests/testdata/TestAgentOnly/call1.golden b/pkg/tests/testdata/TestAgentOnly/call1.golden new file mode 100644 index 00000000..b63c6fd3 --- /dev/null +++ b/pkg/tests/testdata/TestAgentOnly/call1.golden @@ -0,0 +1,42 @@ +`{ + "model": "gpt-4o", + "internalSystemPrompt": false, + "tools": [ + { + "function": { + "toolID": "testdata/TestAgentOnly/test.gpt:agent2", + "name": "agent2", + "parameters": { + "properties": { + "defaultPromptParameter": { + "description": "Prompt to send to the assistant. This may be an instruction or question.", + "type": "string" + } + }, + "type": "object" + } + } + } + ], + "messages": [ + { + "role": "system", + "content": [ + { + "text": "I am agent1" + } + ], + "usage": {} + }, + { + "role": "user", + "content": [ + { + "text": "Input 1" + } + ], + "usage": {} + } + ], + "chat": true +}` diff --git a/pkg/tests/testdata/TestAgentOnly/call2-resp.golden b/pkg/tests/testdata/TestAgentOnly/call2-resp.golden new file mode 100644 index 00000000..997ca1b9 --- /dev/null +++ b/pkg/tests/testdata/TestAgentOnly/call2-resp.golden @@ -0,0 +1,9 @@ +`{ + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 2" + } + ], + "usage": {} +}` diff --git a/pkg/tests/testdata/TestAgentOnly/call2.golden b/pkg/tests/testdata/TestAgentOnly/call2.golden new file mode 100644 index 00000000..82f95523 --- /dev/null +++ b/pkg/tests/testdata/TestAgentOnly/call2.golden @@ -0,0 +1,57 @@ +`{ + "model": "gpt-4o", + "internalSystemPrompt": false, + "tools": [ + { + "function": { + "toolID": "testdata/TestAgentOnly/test.gpt:agent1", + "name": "agent1", + "parameters": { + "properties": { + "defaultPromptParameter": { + "description": "Prompt to send to the assistant. This may be an instruction or question.", + "type": "string" + } + }, + "type": "object" + } + } + }, + { + "function": { + "toolID": "testdata/TestAgentOnly/test.gpt:agent3", + "name": "agent3", + "parameters": { + "properties": { + "defaultPromptParameter": { + "description": "Prompt to send to the assistant. This may be an instruction or question.", + "type": "string" + } + }, + "type": "object" + } + } + } + ], + "messages": [ + { + "role": "system", + "content": [ + { + "text": "I am agent2" + } + ], + "usage": {} + }, + { + "role": "user", + "content": [ + { + "text": "Agent 2 input" + } + ], + "usage": {} + } + ], + "chat": true +}` diff --git a/pkg/tests/testdata/TestAgentOnly/step1.golden b/pkg/tests/testdata/TestAgentOnly/step1.golden new file mode 100644 index 00000000..662dbf04 --- /dev/null +++ b/pkg/tests/testdata/TestAgentOnly/step1.golden @@ -0,0 +1,168 @@ +`{ + "done": false, + "content": "TEST RESULT CALL: 2", + "toolID": "testdata/TestAgentOnly/test.gpt:agent2", + "state": { + "continuation": { + "state": { + "input": "Input 1", + "completion": { + "model": "gpt-4o", + "internalSystemPrompt": false, + "tools": [ + { + "function": { + "toolID": "testdata/TestAgentOnly/test.gpt:agent2", + "name": "agent2", + "parameters": { + "properties": { + "defaultPromptParameter": { + "description": "Prompt to send to the assistant. This may be an instruction or question.", + "type": "string" + } + }, + "type": "object" + } + } + } + ], + "messages": [ + { + "role": "system", + "content": [ + { + "text": "I am agent1" + } + ], + "usage": {} + }, + { + "role": "user", + "content": [ + { + "text": "Input 1" + } + ], + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 0, + "id": "call_1", + "function": { + "name": "agent2", + "arguments": "Agent 2 input" + } + } + } + ], + "usage": {} + } + ], + "chat": true + }, + "pending": { + "call_1": { + "index": 0, + "id": "call_1", + "function": { + "name": "agent2", + "arguments": "Agent 2 input" + } + } + } + }, + "calls": { + "call_1": { + "toolID": "testdata/TestAgentOnly/test.gpt:agent2", + "input": "Agent 2 input" + } + } + }, + "subCalls": [ + { + "toolId": "testdata/TestAgentOnly/test.gpt:agent2", + "callId": "call_1", + "state": { + "continuation": { + "state": { + "input": "Agent 2 input", + "completion": { + "model": "gpt-4o", + "internalSystemPrompt": false, + "tools": [ + { + "function": { + "toolID": "testdata/TestAgentOnly/test.gpt:agent1", + "name": "agent1", + "parameters": { + "properties": { + "defaultPromptParameter": { + "description": "Prompt to send to the assistant. This may be an instruction or question.", + "type": "string" + } + }, + "type": "object" + } + } + }, + { + "function": { + "toolID": "testdata/TestAgentOnly/test.gpt:agent3", + "name": "agent3", + "parameters": { + "properties": { + "defaultPromptParameter": { + "description": "Prompt to send to the assistant. This may be an instruction or question.", + "type": "string" + } + }, + "type": "object" + } + } + } + ], + "messages": [ + { + "role": "system", + "content": [ + { + "text": "I am agent2" + } + ], + "usage": {} + }, + { + "role": "user", + "content": [ + { + "text": "Agent 2 input" + } + ], + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 2" + } + ], + "usage": {} + } + ], + "chat": true + } + }, + "result": "TEST RESULT CALL: 2" + }, + "continuationToolID": "testdata/TestAgentOnly/test.gpt:agent2" + } + } + ], + "subCallID": "call_1" + } +}` diff --git a/pkg/tests/testdata/TestAgentOnly/test.gpt b/pkg/tests/testdata/TestAgentOnly/test.gpt new file mode 100644 index 00000000..f2ae9f9e --- /dev/null +++ b/pkg/tests/testdata/TestAgentOnly/test.gpt @@ -0,0 +1,20 @@ +agents: agent1, agent2 + +---- +name: agent1 +chat: true + +I am agent1 + +---- +name: agent2 +chat: true +agents: agent3 + +I am agent2 + +--- +name: agent3 +chat: true + +I am agent3 \ No newline at end of file diff --git a/pkg/types/tool.go b/pkg/types/tool.go index b0af5183..e4b3424a 100644 --- a/pkg/types/tool.go +++ b/pkg/types/tool.go @@ -763,6 +763,10 @@ func (t Tool) IsOpenAPI() bool { return strings.HasPrefix(t.Instructions, OpenAPIPrefix) } +func (t Tool) IsAgentsOnly() bool { + return t.IsNoop() && len(t.Context) == 0 +} + func (t Tool) IsEcho() bool { return strings.HasPrefix(t.Instructions, EchoPrefix) }