Skip to content

Commit fe26a43

Browse files
committed
deep search integration
1 parent 47598b0 commit fe26a43

20 files changed

Lines changed: 1443 additions & 7 deletions

cmd/src/deepsearch.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package main
2+
3+
import (
4+
"github.com/sourcegraph/src-cli/internal/clicompat"
5+
"github.com/urfave/cli/v3"
6+
)
7+
8+
var deepsearchCommand = clicompat.Wrap(&cli.Command{
9+
Name: "deepsearch",
10+
Aliases: []string{"ds"},
11+
Usage: "interacts with Sourcegraph Deep Search",
12+
UsageText: "src deepsearch [command options]",
13+
Description: deepsearchExamples,
14+
HideVersion: true,
15+
Commands: []*cli.Command{
16+
deepsearchAskCommand,
17+
deepsearchAddQuestionCommand,
18+
deepsearchGetCommand,
19+
deepsearchListCommand,
20+
deepsearchCancelCommand,
21+
deepsearchDeleteCommand,
22+
},
23+
})
24+
25+
const deepsearchExamples = `'src deepsearch' interacts with the Sourcegraph Deep Search API.
26+
27+
Usage:
28+
29+
src deepsearch command [command options]
30+
31+
The commands are:
32+
33+
ask starts a Deep Search conversation and waits for the answer
34+
add-question adds a follow-up question to a conversation
35+
get gets a conversation
36+
list lists conversation summaries
37+
cancel cancels an in-progress conversation
38+
delete permanently deletes a conversation
39+
40+
Use "src deepsearch [command] -h" for more information about a command.
41+
`

cmd/src/deepsearch_add_question.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"strings"
6+
7+
"github.com/sourcegraph/src-cli/internal/clicompat"
8+
"github.com/sourcegraph/src-cli/internal/cmderrors"
9+
"github.com/sourcegraph/src-cli/internal/deepsearch"
10+
"github.com/urfave/cli/v3"
11+
)
12+
13+
const deepsearchAddQuestionExamples = `
14+
Examples:
15+
16+
Ask a follow-up question in an existing conversation:
17+
18+
$ src deepsearch add-question users/-/conversations/abc123 'What calls this code?'
19+
20+
`
21+
22+
var deepsearchAddQuestionCommand = clicompat.Wrap(&cli.Command{
23+
Name: "add-question",
24+
Usage: "adds a follow-up question to a Deep Search conversation",
25+
UsageText: "src deepsearch add-question [options] <conversation-name> <question>",
26+
Description: deepsearchAddQuestionExamples,
27+
HideVersion: true,
28+
Flags: clicompat.WithAPIFlags(
29+
&cli.StringFlag{
30+
Name: "f",
31+
Value: "{{.|json}}",
32+
Usage: `Format for the output, using the syntax of Go package text/template.`,
33+
},
34+
),
35+
Action: func(ctx context.Context, cmd *cli.Command) error {
36+
parent, question, err := deepsearchParentAndQuestion(cmd)
37+
if err != nil {
38+
return err
39+
}
40+
41+
tmpl, err := parseTemplate(cmd.String("f"))
42+
if err != nil {
43+
return err
44+
}
45+
46+
response, ok, err := cfg.deepsearchClient(cmd).AddConversationQuestion(ctx, deepsearch.AddConversationQuestionRequest{
47+
Parent: parent,
48+
Question: deepsearch.NewQuestion(question),
49+
})
50+
if err != nil || !ok {
51+
return err
52+
}
53+
return execTemplate(tmpl, response)
54+
},
55+
})
56+
57+
func deepsearchParentAndQuestion(cmd *cli.Command) (string, string, error) {
58+
args := cmd.Args().Slice()
59+
if len(args) == 0 {
60+
return "", "", cmderrors.Usage("must provide a conversation name")
61+
}
62+
63+
parent := strings.TrimSpace(args[0])
64+
if parent == "" {
65+
return "", "", cmderrors.Usage("must provide a conversation name")
66+
}
67+
68+
question := strings.TrimSpace(strings.Join(args[1:], " "))
69+
if question == "" {
70+
return "", "", cmderrors.Usage("must provide a question")
71+
}
72+
return parent, question, nil
73+
}

cmd/src/deepsearch_ask.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"strings"
8+
"time"
9+
10+
"github.com/sourcegraph/sourcegraph/lib/errors"
11+
12+
"github.com/sourcegraph/src-cli/internal/clicompat"
13+
"github.com/sourcegraph/src-cli/internal/cmderrors"
14+
"github.com/sourcegraph/src-cli/internal/deepsearch"
15+
"github.com/urfave/cli/v3"
16+
)
17+
18+
const deepsearchAskExamples = `
19+
Examples:
20+
21+
Ask a question and wait for the answer:
22+
23+
$ src deepsearch ask 'How is authentication implemented?'
24+
25+
Ask a question using the ds alias:
26+
27+
$ src ds ask 'Which services write repository metadata?'
28+
29+
`
30+
31+
var deepsearchAskCommand = clicompat.Wrap(&cli.Command{
32+
Name: "ask",
33+
Usage: "starts a Deep Search conversation and waits for the answer",
34+
UsageText: "src deepsearch ask [options] <question>",
35+
Description: deepsearchAskExamples,
36+
HideVersion: true,
37+
Flags: clicompat.WithAPIFlags(
38+
&cli.StringFlag{
39+
Name: "parent",
40+
Usage: `Parent resource for the conversation. Defaults to the authenticated user. (e.g. "users/-")`,
41+
},
42+
&cli.DurationFlag{
43+
Name: "timeout",
44+
Value: 5 * time.Minute,
45+
Usage: "Maximum time to wait for an answer.",
46+
},
47+
&cli.DurationFlag{
48+
Name: "poll-interval",
49+
Value: 3 * time.Second,
50+
Usage: "How often to poll for completion.",
51+
},
52+
),
53+
Action: func(ctx context.Context, cmd *cli.Command) error {
54+
question, err := deepsearchQuestion(cmd)
55+
if err != nil {
56+
return err
57+
}
58+
timeout := cmd.Duration("timeout")
59+
if timeout <= 0 {
60+
return cmderrors.Usage("timeout must be greater than 0")
61+
}
62+
pollInterval := cmd.Duration("poll-interval")
63+
if pollInterval <= 0 {
64+
return cmderrors.Usage("poll-interval must be greater than 0")
65+
}
66+
67+
client := cfg.deepsearchClient(cmd)
68+
conversation, ok, err := client.CreateConversation(ctx, deepsearch.CreateConversationRequest{
69+
Parent: cmd.String("parent"),
70+
Conversation: deepsearch.Conversation{
71+
Questions: []deepsearch.Question{deepsearch.NewQuestion(question)},
72+
},
73+
})
74+
if err != nil || !ok {
75+
return err
76+
}
77+
78+
return waitForDeepsearchAnswer(ctx, cmd.Writer, client, conversation, timeout, pollInterval)
79+
},
80+
})
81+
82+
func deepsearchQuestion(cmd *cli.Command) (string, error) {
83+
question := strings.TrimSpace(strings.Join(cmd.Args().Slice(), " "))
84+
if question == "" {
85+
return "", cmderrors.Usage("must provide a question")
86+
}
87+
return question, nil
88+
}
89+
90+
func waitForDeepsearchAnswer(ctx context.Context, out io.Writer, client *deepsearch.Client, conversation *deepsearch.Conversation, timeout, pollInterval time.Duration) error {
91+
ctx, cancel := context.WithTimeout(ctx, timeout)
92+
defer cancel()
93+
ticker := time.NewTicker(pollInterval)
94+
defer ticker.Stop()
95+
96+
for {
97+
done, err := deepsearchDone(conversation)
98+
if err != nil {
99+
return err
100+
}
101+
if done {
102+
return printDeepsearchAnswer(out, conversation)
103+
}
104+
if conversation.Name == "" {
105+
return errors.New("deep search response did not include a conversation name")
106+
}
107+
108+
select {
109+
case <-ctx.Done():
110+
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
111+
return errors.Newf("timed out waiting for Deep Search answer after %s", timeout)
112+
}
113+
return ctx.Err()
114+
case <-ticker.C:
115+
}
116+
117+
var ok bool
118+
conversation, ok, err = client.GetConversation(ctx, deepsearch.GetConversationRequest{Name: conversation.Name})
119+
if err != nil || !ok {
120+
return err
121+
}
122+
}
123+
}
124+
125+
func deepsearchDone(conversation *deepsearch.Conversation) (bool, error) {
126+
if conversation == nil || conversation.State == nil || conversation.State.Processing != nil {
127+
return false, nil
128+
}
129+
if conversation.State.Completed != nil {
130+
return true, nil
131+
}
132+
if conversation.State.Canceled != nil {
133+
return false, errors.New("Deep Search conversation was canceled")
134+
}
135+
if conversation.State.Error != nil {
136+
message := conversation.State.Error.Message
137+
if message == "" {
138+
message = conversation.State.Error.Code
139+
}
140+
if message == "" {
141+
message = "unknown error"
142+
}
143+
return false, errors.Newf("Deep Search failed: %s", message)
144+
}
145+
return false, nil
146+
}
147+
148+
func printDeepsearchAnswer(out io.Writer, conversation *deepsearch.Conversation) error {
149+
for _, question := range conversation.Questions {
150+
for _, answer := range question.Answer {
151+
if answer.Markdown == nil {
152+
continue
153+
}
154+
if _, err := fmt.Fprint(out, answer.Markdown.Text); err != nil {
155+
return err
156+
}
157+
if !strings.HasSuffix(answer.Markdown.Text, "\n") {
158+
if _, err := fmt.Fprintln(out); err != nil {
159+
return err
160+
}
161+
}
162+
}
163+
}
164+
return nil
165+
}

cmd/src/deepsearch_cancel.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package main
2+
3+
import (
4+
"context"
5+
6+
"github.com/sourcegraph/src-cli/internal/clicompat"
7+
"github.com/sourcegraph/src-cli/internal/deepsearch"
8+
"github.com/urfave/cli/v3"
9+
)
10+
11+
const deepsearchCancelExamples = `
12+
Examples:
13+
14+
Cancel a conversation:
15+
16+
$ src deepsearch cancel users/-/conversations/abc123
17+
18+
`
19+
20+
var deepsearchCancelCommand = clicompat.Wrap(&cli.Command{
21+
Name: "cancel",
22+
Usage: "cancels an in-progress Deep Search conversation",
23+
UsageText: "src deepsearch cancel [options] <conversation-name>",
24+
Description: deepsearchCancelExamples,
25+
HideVersion: true,
26+
Flags: clicompat.WithAPIFlags(
27+
&cli.StringFlag{
28+
Name: "f",
29+
Value: "{{.|json}}",
30+
Usage: `Format for the output, using the syntax of Go package text/template.`,
31+
},
32+
),
33+
Action: func(ctx context.Context, cmd *cli.Command) error {
34+
name, err := deepsearchName(cmd)
35+
if err != nil {
36+
return err
37+
}
38+
tmpl, err := parseTemplate(cmd.String("f"))
39+
if err != nil {
40+
return err
41+
}
42+
43+
conversation, ok, err := cfg.deepsearchClient(cmd).CancelConversation(ctx, deepsearch.CancelConversationRequest{Name: name})
44+
if err != nil || !ok {
45+
return err
46+
}
47+
return execTemplate(tmpl, conversation)
48+
},
49+
})

cmd/src/deepsearch_client.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package main
2+
3+
import (
4+
"github.com/sourcegraph/src-cli/internal/api/connect"
5+
"github.com/sourcegraph/src-cli/internal/clicompat"
6+
"github.com/sourcegraph/src-cli/internal/deepsearch"
7+
"github.com/urfave/cli/v3"
8+
)
9+
10+
func (c *config) connectClient(cmd *cli.Command) connect.Client {
11+
flags := clicompat.APIFlagsFromCmd(cmd)
12+
return connect.NewClient(c.apiClient(flags, cmd.Writer))
13+
}
14+
15+
func (c *config) deepsearchClient(cmd *cli.Command) *deepsearch.Client {
16+
return deepsearch.NewClient(c.connectClient(cmd))
17+
}

cmd/src/deepsearch_delete.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/sourcegraph/src-cli/internal/clicompat"
8+
"github.com/sourcegraph/src-cli/internal/deepsearch"
9+
"github.com/urfave/cli/v3"
10+
)
11+
12+
const deepsearchDeleteExamples = `
13+
Examples:
14+
15+
Permanently delete a conversation:
16+
17+
$ src deepsearch delete users/-/conversations/abc123
18+
19+
`
20+
21+
var deepsearchDeleteCommand = clicompat.Wrap(&cli.Command{
22+
Name: "delete",
23+
Usage: "permanently deletes a Deep Search conversation",
24+
UsageText: "src deepsearch delete [options] <conversation-name>",
25+
Description: deepsearchDeleteExamples,
26+
HideVersion: true,
27+
Flags: clicompat.WithAPIFlags(),
28+
Action: func(ctx context.Context, cmd *cli.Command) error {
29+
name, err := deepsearchName(cmd)
30+
if err != nil {
31+
return err
32+
}
33+
34+
ok, err := cfg.deepsearchClient(cmd).DeleteConversation(ctx, deepsearch.DeleteConversationRequest{Name: name})
35+
if err != nil || !ok {
36+
return err
37+
}
38+
_, err = fmt.Fprintf(cmd.Writer, "Deep Search conversation %q deleted.\n", name)
39+
return err
40+
},
41+
})

0 commit comments

Comments
 (0)