Skip to content

Commit 1b7c477

Browse files
chore: add multiline parsing and refactor share cred behavior
1 parent cc5e5ed commit 1b7c477

File tree

7 files changed

+291
-38
lines changed

7 files changed

+291
-38
lines changed

pkg/config/cliconfig.go

-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ func (a *AuthConfig) UnmarshalJSON(data []byte) error {
7373
type CLIConfig struct {
7474
Auths map[string]AuthConfig `json:"auths,omitempty"`
7575
CredentialsStore string `json:"credsStore,omitempty"`
76-
GatewayURL string `json:"gatewayURL,omitempty"`
7776
Integrations map[string]string `json:"integrations,omitempty"`
7877

7978
auths map[string]types.AuthConfig

pkg/credentials/store.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ func validateCredentialCtx(ctxs []string) error {
225225
}
226226

227227
// check alphanumeric
228-
r := regexp.MustCompile("^[a-zA-Z0-9]+$")
228+
r := regexp.MustCompile("^[-a-zA-Z0-9]+$")
229229
for _, c := range ctxs {
230230
if !r.MatchString(c) {
231231
return fmt.Errorf("credential contexts must be alphanumeric")

pkg/parser/parser.go

+88-21
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package parser
22

33
import (
4-
"bufio"
54
"fmt"
65
"io"
76
"maps"
@@ -17,8 +16,10 @@ import (
1716

1817
var (
1918
sepRegex = regexp.MustCompile(`^\s*---+\s*$`)
19+
endHeaderRegex = regexp.MustCompile(`^\s*===+\s*$`)
2020
strictSepRegex = regexp.MustCompile(`^---\n$`)
2121
skipRegex = regexp.MustCompile(`^![-.:*\w]+\s*$`)
22+
nameRegex = regexp.MustCompile(`^[a-z]+$`)
2223
)
2324

2425
func normalize(key string) string {
@@ -56,6 +57,9 @@ func addArg(line string, tool *types.Tool) error {
5657
tool.Parameters.Arguments = &openapi3.Schema{
5758
Type: &openapi3.Types{"object"},
5859
Properties: openapi3.Schemas{},
60+
AdditionalProperties: openapi3.AdditionalProperties{
61+
Has: new(bool),
62+
},
5963
}
6064
}
6165

@@ -74,7 +78,7 @@ func addArg(line string, tool *types.Tool) error {
7478
return nil
7579
}
7680

77-
func isParam(line string, tool *types.Tool) (_ bool, err error) {
81+
func isParam(line string, tool *types.Tool, scan *simplescanner) (_ bool, err error) {
7882
key, value, ok := strings.Cut(line, ":")
7983
if !ok {
8084
return false, nil
@@ -90,7 +94,7 @@ func isParam(line string, tool *types.Tool) (_ bool, err error) {
9094
case "globalmodel", "globalmodelname":
9195
tool.Parameters.GlobalModelName = value
9296
case "description":
93-
tool.Parameters.Description = value
97+
tool.Parameters.Description = scan.AddMultiline(value)
9498
case "internalprompt":
9599
v, err := toBool(value)
96100
if err != nil {
@@ -104,27 +108,33 @@ func isParam(line string, tool *types.Tool) (_ bool, err error) {
104108
}
105109
tool.Parameters.Chat = v
106110
case "export", "exporttool", "exports", "exporttools", "sharetool", "sharetools", "sharedtool", "sharedtools":
107-
tool.Parameters.Export = append(tool.Parameters.Export, csv(value)...)
111+
tool.Parameters.Export = append(tool.Parameters.Export, csv(scan.AddMultiline(value))...)
108112
case "tool", "tools":
109-
tool.Parameters.Tools = append(tool.Parameters.Tools, csv(value)...)
113+
tool.Parameters.Tools = append(tool.Parameters.Tools, csv(scan.AddMultiline(value))...)
110114
case "inputfilter", "inputfilters":
111-
tool.Parameters.InputFilters = append(tool.Parameters.InputFilters, csv(value)...)
115+
tool.Parameters.InputFilters = append(tool.Parameters.InputFilters, csv(scan.AddMultiline(value))...)
112116
case "shareinputfilter", "shareinputfilters", "sharedinputfilter", "sharedinputfilters":
113-
tool.Parameters.ExportInputFilters = append(tool.Parameters.ExportInputFilters, csv(value)...)
117+
tool.Parameters.ExportInputFilters = append(tool.Parameters.ExportInputFilters, csv(scan.AddMultiline(value))...)
114118
case "outputfilter", "outputfilters":
115-
tool.Parameters.OutputFilters = append(tool.Parameters.OutputFilters, csv(value)...)
119+
tool.Parameters.OutputFilters = append(tool.Parameters.OutputFilters, csv(scan.AddMultiline(value))...)
116120
case "shareoutputfilter", "shareoutputfilters", "sharedoutputfilter", "sharedoutputfilters":
117-
tool.Parameters.ExportOutputFilters = append(tool.Parameters.ExportOutputFilters, csv(value)...)
121+
tool.Parameters.ExportOutputFilters = append(tool.Parameters.ExportOutputFilters, csv(scan.AddMultiline(value))...)
118122
case "agent", "agents":
119-
tool.Parameters.Agents = append(tool.Parameters.Agents, csv(value)...)
123+
tool.Parameters.Agents = append(tool.Parameters.Agents, csv(scan.AddMultiline(value))...)
120124
case "globaltool", "globaltools":
121-
tool.Parameters.GlobalTools = append(tool.Parameters.GlobalTools, csv(value)...)
125+
tool.Parameters.GlobalTools = append(tool.Parameters.GlobalTools, csv(scan.AddMultiline(value))...)
122126
case "exportcontext", "exportcontexts", "sharecontext", "sharecontexts", "sharedcontext", "sharedcontexts":
123-
tool.Parameters.ExportContext = append(tool.Parameters.ExportContext, csv(value)...)
127+
tool.Parameters.ExportContext = append(tool.Parameters.ExportContext, csv(scan.AddMultiline(value))...)
124128
case "context":
125-
tool.Parameters.Context = append(tool.Parameters.Context, csv(value)...)
129+
tool.Parameters.Context = append(tool.Parameters.Context, csv(scan.AddMultiline(value))...)
130+
case "metadata":
131+
mkey, mvalue, _ := strings.Cut(scan.AddMultiline(value), ":")
132+
if tool.MetaData == nil {
133+
tool.MetaData = map[string]string{}
134+
}
135+
tool.MetaData[strings.TrimSpace(mkey)] = strings.TrimSpace(mvalue)
126136
case "args", "arg", "param", "params", "parameters", "parameter":
127-
if err := addArg(value, tool); err != nil {
137+
if err := addArg(scan.AddMultiline(value), tool); err != nil {
128138
return false, err
129139
}
130140
case "maxtoken", "maxtokens":
@@ -149,13 +159,13 @@ func isParam(line string, tool *types.Tool) (_ bool, err error) {
149159
return false, err
150160
}
151161
case "credentials", "creds", "credential", "cred":
152-
tool.Parameters.Credentials = append(tool.Parameters.Credentials, value)
162+
tool.Parameters.Credentials = append(tool.Parameters.Credentials, csv(scan.AddMultiline(value))...)
153163
case "sharecredentials", "sharecreds", "sharecredential", "sharecred", "sharedcredentials", "sharedcreds", "sharedcredential", "sharedcred":
154-
tool.Parameters.ExportCredentials = append(tool.Parameters.ExportCredentials, value)
164+
tool.Parameters.ExportCredentials = append(tool.Parameters.ExportCredentials, scan.AddMultiline(value))
155165
case "type":
156166
tool.Type = types.ToolType(strings.ToLower(value))
157167
default:
158-
return false, nil
168+
return nameRegex.MatchString(key), nil
159169
}
160170

161171
return true, nil
@@ -206,6 +216,7 @@ func (c *context) finish(tools *[]Node) {
206216
len(c.tool.ExportInputFilters) > 0 ||
207217
len(c.tool.ExportOutputFilters) > 0 ||
208218
len(c.tool.Agents) > 0 ||
219+
len(c.tool.ExportCredentials) > 0 ||
209220
c.tool.Chat {
210221
*tools = append(*tools, Node{
211222
ToolNode: &ToolNode{
@@ -391,7 +402,10 @@ func assignMetadata(nodes []Node) (result []Node) {
391402

392403
for _, node := range nodes {
393404
if node.ToolNode != nil {
394-
node.ToolNode.Tool.MetaData = metadata[node.ToolNode.Tool.Name]
405+
if node.ToolNode.Tool.MetaData == nil {
406+
node.ToolNode.Tool.MetaData = map[string]string{}
407+
}
408+
maps.Copy(node.ToolNode.Tool.MetaData, metadata[node.ToolNode.Tool.Name])
395409
for wildcard := range metadata {
396410
if strings.Contains(wildcard, "*") {
397411
if m, err := path.Match(wildcard, node.ToolNode.Tool.Name); m && err == nil {
@@ -433,15 +447,64 @@ func isGPTScriptHashBang(line string) bool {
433447
return false
434448
}
435449

436-
func parse(input io.Reader) ([]Node, error) {
437-
scan := bufio.NewScanner(input)
450+
type simplescanner struct {
451+
lines []string
452+
}
453+
454+
func newSimpleScanner(data []byte) *simplescanner {
455+
if len(data) == 0 {
456+
return &simplescanner{}
457+
}
458+
lines := strings.Split(string(data), "\n")
459+
return &simplescanner{
460+
lines: append([]string{""}, lines...),
461+
}
462+
}
463+
464+
func (s *simplescanner) AddMultiline(current string) string {
465+
result := current
466+
for {
467+
if len(s.lines) < 2 || len(s.lines[1]) == 0 {
468+
return result
469+
}
470+
if strings.HasPrefix(s.lines[1], " ") || strings.HasPrefix(s.lines[1], "\t") {
471+
result += " " + s.lines[1]
472+
s.lines = s.lines[1:]
473+
} else {
474+
return result
475+
}
476+
}
477+
}
438478

479+
func (s *simplescanner) Text() string {
480+
if len(s.lines) == 0 {
481+
return ""
482+
}
483+
return s.lines[0]
484+
}
485+
486+
func (s *simplescanner) Scan() bool {
487+
if len(s.lines) == 0 {
488+
return false
489+
}
490+
s.lines = s.lines[1:]
491+
return true
492+
}
493+
494+
func parse(input io.Reader) ([]Node, error) {
439495
var (
440496
tools []Node
441497
context context
442498
lineNo int
443499
)
444500

501+
data, err := io.ReadAll(input)
502+
if err != nil {
503+
return nil, err
504+
}
505+
506+
scan := newSimpleScanner(data)
507+
445508
for scan.Scan() {
446509
lineNo++
447510
if context.tool.Source.LineNo == 0 {
@@ -488,11 +551,15 @@ func parse(input io.Reader) ([]Node, error) {
488551
}
489552

490553
// Look for params
491-
if isParam, err := isParam(line, &context.tool); err != nil {
554+
if isParam, err := isParam(line, &context.tool, scan); err != nil {
492555
return nil, NewErrLine("", lineNo, err)
493556
} else if isParam {
494557
context.seenParam = true
495558
continue
559+
} else if endHeaderRegex.MatchString(line) {
560+
// force the end of the header and don't include the current line in the header
561+
context.inBody = true
562+
continue
496563
}
497564
}
498565

pkg/parser/parser_test.go

+91
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package parser
22

33
import (
4+
"reflect"
45
"strings"
56
"testing"
67

@@ -244,6 +245,7 @@ share output filters: shared
244245
func TestParseMetaData(t *testing.T) {
245246
input := `
246247
name: first
248+
metadata: foo: bar
247249
248250
body
249251
---
@@ -269,8 +271,97 @@ foo bar
269271

270272
assert.Len(t, tools, 1)
271273
autogold.Expect(map[string]string{
274+
"foo": "bar",
272275
"package.json": "foo=base\nf",
273276
"requirements.txt": "asdf",
274277
"other": "foo bar",
275278
}).Equal(t, tools[0].MetaData)
279+
280+
autogold.Expect(`Name: first
281+
Meta Data: foo: bar
282+
Meta Data: other: foo bar
283+
Meta Data: requirements.txt: asdf
284+
285+
body
286+
---
287+
!metadata:first:package.json
288+
foo=base
289+
f
290+
`).Equal(t, tools[0].String())
291+
}
292+
293+
func TestFormatWithBadInstruction(t *testing.T) {
294+
input := types.Tool{
295+
ToolDef: types.ToolDef{
296+
Parameters: types.Parameters{
297+
Name: "foo",
298+
},
299+
Instructions: "foo: bar",
300+
},
301+
}
302+
autogold.Expect("Name: foo\n===\nfoo: bar\n").Equal(t, input.String())
303+
304+
tools, err := ParseTools(strings.NewReader(input.String()))
305+
require.NoError(t, err)
306+
if reflect.DeepEqual(input, tools[0]) {
307+
t.Errorf("expected %v, got %v", input, tools[0])
308+
}
309+
}
310+
311+
func TestSingleTool(t *testing.T) {
312+
input := `
313+
name: foo
314+
315+
#!sys.echo
316+
hi
317+
`
318+
319+
tools, err := ParseTools(strings.NewReader(input))
320+
require.NoError(t, err)
321+
autogold.Expect(types.Tool{
322+
ToolDef: types.ToolDef{
323+
Parameters: types.Parameters{
324+
Name: "first",
325+
ModelName: "the model",
326+
Credentials: []string{
327+
"foo",
328+
"bar",
329+
"baz",
330+
},
331+
},
332+
Instructions: "body",
333+
},
334+
Source: types.ToolSource{LineNo: 1},
335+
}).Equal(t, tools[0])
336+
}
337+
338+
func TestMultiline(t *testing.T) {
339+
input := `
340+
name: first
341+
credential: foo
342+
, bar,
343+
baz
344+
model: the model
345+
346+
body
347+
`
348+
tools, err := ParseTools(strings.NewReader(input))
349+
require.NoError(t, err)
350+
351+
assert.Len(t, tools, 1)
352+
autogold.Expect(types.Tool{
353+
ToolDef: types.ToolDef{
354+
Parameters: types.Parameters{
355+
Name: "first",
356+
ModelName: "the model",
357+
Credentials: []string{
358+
"foo",
359+
"bar",
360+
"baz",
361+
},
362+
},
363+
Instructions: "body",
364+
},
365+
Source: types.ToolSource{LineNo: 1},
366+
}).Equal(t, tools[0])
276367
}

pkg/tests/runner2_test.go

+32
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,35 @@ echo ${FOO}:${INPUT}
7979
resp, err = r.Chat(context.Background(), nil, prg, nil, `"foo":"123"}`)
8080
r.AssertStep(t, resp, err)
8181
}
82+
83+
func TestShareCreds(t *testing.T) {
84+
r := tester.NewRunner(t)
85+
prg, err := loader.ProgramFromSource(context.Background(), `
86+
creds: foo
87+
88+
#!/bin/bash
89+
echo $CRED
90+
echo $CRED2
91+
92+
---
93+
name: foo
94+
share credentials: bar
95+
96+
---
97+
name: bar
98+
share credentials: baz
99+
100+
#!/bin/bash
101+
echo '{"env": {"CRED": "that worked"}}'
102+
103+
---
104+
name: baz
105+
106+
#!/bin/bash
107+
echo '{"env": {"CRED2": "that also worked"}}'
108+
`, "")
109+
require.NoError(t, err)
110+
111+
resp, err := r.Chat(context.Background(), nil, prg, nil, "")
112+
r.AssertStep(t, resp, err)
113+
}

pkg/types/completion.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"fmt"
55
"strings"
66

7-
"github.com/fatih/color"
87
"github.com/getkin/kin-openapi/openapi3"
98
)
109

@@ -112,7 +111,7 @@ func (c CompletionMessage) String() string {
112111
}
113112
buf.WriteString(content.Text)
114113
if content.ToolCall != nil {
115-
buf.WriteString(fmt.Sprintf("<tool call> %s -> %s", color.GreenString(content.ToolCall.Function.Name), content.ToolCall.Function.Arguments))
114+
buf.WriteString(fmt.Sprintf("<tool call> %s -> %s", content.ToolCall.Function.Name, content.ToolCall.Function.Arguments))
116115
}
117116
}
118117
return buf.String()

0 commit comments

Comments
 (0)