Skip to content

Commit e582ac1

Browse files
committed
Add interactive mode for index creation
1 parent a704842 commit e582ac1

File tree

6 files changed

+649
-4
lines changed

6 files changed

+649
-4
lines changed

internal/pkg/cli/command/index/create.go

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package index
22

33
import (
4+
"context"
45
"errors"
56
"fmt"
7+
"strings"
68

79
"github.com/MakeNowJust/heredoc"
810
"github.com/pinecone-io/cli/internal/pkg/utils/docslinks"
@@ -12,6 +14,7 @@ import (
1214
indexpresenters "github.com/pinecone-io/cli/internal/pkg/utils/index/presenters"
1315
"github.com/pinecone-io/cli/internal/pkg/utils/interactive"
1416
"github.com/pinecone-io/cli/internal/pkg/utils/log"
17+
"github.com/pinecone-io/cli/internal/pkg/utils/models"
1518
"github.com/pinecone-io/cli/internal/pkg/utils/msg"
1619
"github.com/pinecone-io/cli/internal/pkg/utils/pcio"
1720
"github.com/pinecone-io/cli/internal/pkg/utils/sdk"
@@ -24,6 +27,7 @@ import (
2427
type createIndexOptions struct {
2528
CreateOptions index.CreateOptions
2629
json bool
30+
interactive bool
2731
}
2832

2933
func NewCreateIndexCmd() *cobra.Command {
@@ -43,10 +47,14 @@ func NewCreateIndexCmd() *cobra.Command {
4347
4448
The CLI will try to automatically infer missing settings from those provided.
4549
50+
Use the %s flag to enable interactive mode, which will guide you through configuring
51+
the index settings step by step.
52+
4653
For detailed documentation, see:
4754
%s
4855
`, style.Code("pc index create"),
4956
style.Emphasis("--model"),
57+
style.Code("--interactive"),
5058
style.URL(docslinks.DocsIndexCreate)),
5159
Example: heredoc.Doc(`
5260
# create default index (serverless)
@@ -67,6 +75,9 @@ func NewCreateIndexCmd() *cobra.Command {
6775
# create a serverless index with the default sparse model
6876
$ pc index create my-index --model sparse --cloud aws --region us-east-1
6977
78+
# create an index using interactive mode
79+
$ pc index create my-index --interactive
80+
7081
`),
7182
Args: index.ValidateIndexNameArgs,
7283
SilenceUsage: true,
@@ -110,13 +121,265 @@ func NewCreateIndexCmd() *cobra.Command {
110121
cmd.Flags().StringToStringVar(&options.CreateOptions.Tags.Value, "tags", map[string]string{}, "Custom user tags to add to an index")
111122

112123
cmd.Flags().BoolVar(&options.json, "json", false, "Output as JSON")
124+
cmd.Flags().BoolVarP(&options.interactive, "interactive", "i", false, "Enable interactive mode to configure index settings step by step")
113125

114126
return cmd
115127
}
116128

129+
func collectInteractiveConfiguration(ctx context.Context, options index.CreateOptions) (index.CreateOptions, bool) {
130+
pcio.Println(style.InfoMsg("Interactive mode: Let's configure your index step by step.\n"))
131+
pcio.Println(style.Hint("Press Esc or Ctrl+C at any time to exit interactive mode.\n"))
132+
133+
// Variables for model data (will be populated when needed)
134+
var availableModels []models.ModelInfo
135+
var modelsErr error
136+
137+
// Index type selection - determine default based on existing flags
138+
var defaultChoice string
139+
if options.Serverless.Value {
140+
defaultChoice = "Serverless"
141+
} else if options.Pod.Value {
142+
defaultChoice = "Pod"
143+
} else {
144+
defaultChoice = "Serverless"
145+
}
146+
147+
choice, exit := interactive.GetChoice(
148+
"Select index type",
149+
[]string{
150+
"Serverless",
151+
"Pod",
152+
},
153+
defaultChoice)
154+
155+
if exit {
156+
return options, true
157+
}
158+
159+
switch choice {
160+
case "Serverless":
161+
options.Serverless.Value = true
162+
options.Pod.Value = false
163+
case "Pod":
164+
options.Serverless.Value = false
165+
options.Pod.Value = true
166+
}
167+
168+
// Serverless configuration
169+
if options.Serverless.Value {
170+
// Fetch available models for serverless
171+
availableModels, modelsErr = models.GetModels(ctx, true)
172+
if modelsErr != nil {
173+
pcio.Println(style.WarnMsg("Warning: Could not fetch available models!"))
174+
}
175+
176+
// Model selection
177+
if modelsErr != nil {
178+
options.Model.Value = "llama-text-embed-v2"
179+
} else {
180+
// Create model choices
181+
modelChoices := make([]string, 0, len(availableModels)+1)
182+
modelChoices = append(modelChoices, "None (custom vectors)")
183+
184+
for _, model := range availableModels {
185+
modelChoices = append(modelChoices, model.Model)
186+
}
187+
188+
// Determine default model choice
189+
var defaultModelChoice string
190+
if options.Model.Value != "" {
191+
defaultModelChoice = options.Model.Value
192+
} else {
193+
defaultModelChoice = "llama-text-embed-v2" // Default dense model
194+
}
195+
196+
modelChoice, exit := interactive.GetChoice("Select inference model", modelChoices, defaultModelChoice)
197+
if exit {
198+
return options, true
199+
}
200+
201+
if modelChoice == "None (custom vectors)" {
202+
options.Model.Value = ""
203+
} else {
204+
options.Model.Value = modelChoice
205+
}
206+
}
207+
208+
// Cloud and region
209+
cloud, exit := interactive.GetInput("Cloud provider (aws, gcp, azure)", options.Cloud.Value)
210+
if exit {
211+
return options, true
212+
}
213+
options.Cloud.Value = cloud
214+
215+
region, exit := interactive.GetInput("Region (e.g., us-east-1)", options.Region.Value)
216+
if exit {
217+
return options, true
218+
}
219+
options.Region.Value = region
220+
221+
// Vector type (only for serverless without model)
222+
if options.Model.Value == "" {
223+
vectorType, exit := interactive.GetChoice("Vector type", []string{"dense", "sparse"}, options.VectorType.Value)
224+
if exit {
225+
return options, true
226+
}
227+
options.VectorType.Value = vectorType
228+
}
229+
}
230+
231+
// Environment (for pod)
232+
if options.Pod.Value {
233+
environment, exit := interactive.GetInput("Environment", options.Environment.Value)
234+
if exit {
235+
return options, true
236+
}
237+
options.Environment.Value = environment
238+
239+
podType, exit := interactive.GetInput("Pod type", options.PodType.Value)
240+
if exit {
241+
return options, true
242+
}
243+
options.PodType.Value = podType
244+
245+
shards, exit := interactive.GetIntInput("Number of shards", int(options.Shards.Value))
246+
if exit {
247+
return options, true
248+
}
249+
options.Shards.Value = int32(shards)
250+
251+
replicas, exit := interactive.GetIntInput("Number of replicas", int(options.Replicas.Value))
252+
if exit {
253+
return options, true
254+
}
255+
options.Replicas.Value = int32(replicas)
256+
}
257+
258+
// Common settings
259+
260+
// Handle dimension based on vector type
261+
// Sparse models always use dimension 0, dense models may support multiple dimensions
262+
isSparse := false
263+
if options.Model.Value != "" {
264+
// Check if the selected model is sparse
265+
if modelsErr == nil {
266+
for _, model := range availableModels {
267+
if model.Model == options.Model.Value && model.VectorType != nil && *model.VectorType == "sparse" {
268+
isSparse = true
269+
break
270+
}
271+
}
272+
}
273+
} else {
274+
// For custom vectors, check the vector type
275+
isSparse = options.VectorType.Value == "sparse"
276+
}
277+
278+
if isSparse {
279+
// Sparse vectors always use dimension 0
280+
options.Dimension.Value = 0
281+
} else {
282+
// Ask for dimension for dense models and custom vectors
283+
dimension, exit := interactive.GetIntInput("Dimension (0 for auto)", int(options.Dimension.Value))
284+
if exit {
285+
return options, true
286+
}
287+
options.Dimension.Value = int32(dimension)
288+
}
289+
290+
// Only ask for metric if not sparse (sparse always uses dotproduct)
291+
// isSparse is already determined above
292+
293+
if !isSparse {
294+
metric, exit := interactive.GetChoice("Metric", []string{"cosine", "euclidean", "dotproduct"}, options.Metric.Value)
295+
if exit {
296+
return options, true
297+
}
298+
options.Metric.Value = metric
299+
} else {
300+
// Sparse vectors always use dotproduct
301+
options.Metric.Value = "dotproduct"
302+
}
303+
304+
// Set default deletion protection to disabled if not already set
305+
defaultDeletionProtection := options.DeletionProtection.Value
306+
if defaultDeletionProtection == "" {
307+
defaultDeletionProtection = "disabled"
308+
}
309+
310+
deletionProtection, exit := interactive.GetChoice("Deletion protection", []string{"enabled", "disabled"}, defaultDeletionProtection)
311+
if exit {
312+
return options, true
313+
}
314+
options.DeletionProtection.Value = deletionProtection
315+
316+
// Tags
317+
useTags := interactive.GetConfirmation("Add custom tags?")
318+
319+
if useTags {
320+
// Initialize tags map if it doesn't exist
321+
if options.Tags.Value == nil {
322+
options.Tags.Value = make(map[string]string)
323+
}
324+
325+
pcio.Println("Enter tags in key=value format. Press Enter with empty input to finish.")
326+
327+
for {
328+
// Show current tags
329+
if len(options.Tags.Value) > 0 {
330+
for k, v := range options.Tags.Value {
331+
pcio.Printf(" %s=%s\n", style.Emphasis(k), style.ResourceName(v))
332+
}
333+
pcio.Println()
334+
}
335+
336+
tagInput, exit := interactive.GetInput("Tag (key=value)", "")
337+
if exit {
338+
return options, true
339+
}
340+
341+
// Empty input means done adding tags
342+
if strings.TrimSpace(tagInput) == "" {
343+
break
344+
}
345+
346+
// Parse key=value format
347+
parts := strings.SplitN(tagInput, "=", 2)
348+
if len(parts) != 2 {
349+
pcio.Println(style.FailMsg("Invalid format. Please use key=value format."))
350+
continue
351+
}
352+
353+
key := strings.TrimSpace(parts[0])
354+
value := strings.TrimSpace(parts[1])
355+
356+
if key == "" || value == "" {
357+
pcio.Println(style.FailMsg("Both key and value must be non-empty."))
358+
continue
359+
}
360+
361+
// Add the tag
362+
options.Tags.Value[key] = value
363+
}
364+
}
365+
366+
pcio.Println(style.SuccessMsg("\nConfiguration complete!"))
367+
return options, false
368+
}
369+
117370
func runCreateIndexCmd(options createIndexOptions, cmd *cobra.Command, args []string) {
118371
ctx := cmd.Context()
119372

373+
// If interactive mode is enabled, collect configuration interactively
374+
if options.interactive {
375+
var exit bool
376+
options.CreateOptions, exit = collectInteractiveConfiguration(ctx, options.CreateOptions)
377+
if exit {
378+
pcio.Println(style.InfoMsg("Interactive mode cancelled."))
379+
return
380+
}
381+
}
382+
120383
// validationErrors := index.ValidateCreateOptions(options.CreateOptions)
121384
// if len(validationErrors) > 0 {
122385
// msg.FailMsgMultiLine(validationErrors...)

0 commit comments

Comments
 (0)