Skip to content

Commit 029d04d

Browse files
prachi-shah-harnessHarness
authored andcommitted
fix: [ML-1268]: fetch prompts based on module and mode (#113)
* add appropriate error messgaes * modify test * add cd module architect mode pormpt and change prompt name to uppercase module id * Merge branch 'master' into ML-1268 * added test for register prompts method * modify go directive * allow architect mode only for internal mode * allow architect mode only for internal mode * merge * change file structure and names * change to argument passing logic * fix: [ML-1268]: fetch prompts based on module and mode
1 parent eada606 commit 029d04d

File tree

6 files changed

+342
-65
lines changed

6 files changed

+342
-65
lines changed

pkg/harness/prompts/fetcher.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,24 @@ import (
44
"embed"
55
)
66

7-
//go:embed files/**/*.txt
7+
//go:embed files/internal/**/*.txt files/**/*.txt
88
var PromptFiles embed.FS // Exported to be accessible from other packages
99

1010
// GetModulePrompts retrieves prompts for a specific module from a given filesystem
1111
// GetModulePrompts retrieves prompts for a specific module from a given filesystem.
1212
// It loads common prompts from the "files/common" directory and mode-specific prompts
1313
// from the "files/internal" or "files/external" directory, depending on the value of `isInternal`.
1414
// It returns a slice of PromptFile containing the loaded prompts.
15-
func GetModulePrompts(fs embed.FS, module string, isInternal bool) ([]PromptFile, error) {
15+
func GetModulePrompts(fs embed.FS, module string, isInternal bool, mode string) ([]PromptFile, error) {
1616
// Use embedded filesystem instead of file system paths
1717
// Use the provided embedded filesystem
1818
fileLoader := NewEmbedFileLoader(fs)
1919

2020
var allPrompts []PromptFile
2121

22+
filename := module + ".txt"
2223
// Load common prompt for the module directly
23-
commonPath := "files/common/" + module + ".txt"
24+
commonPath := "files/common/" + filename
2425
commonPrompt, err := fileLoader.loadPromptFileFromEmbed(commonPath)
2526
if err == nil {
2627
allPrompts = append(allPrompts, commonPrompt)
@@ -29,9 +30,9 @@ func GetModulePrompts(fs embed.FS, module string, isInternal bool) ([]PromptFile
2930
// Load mode-specific prompt for the module directly
3031
var modePath string
3132
if isInternal {
32-
modePath = "files/internal/" + module + ".txt"
33+
modePath = "files/internal/" + mode + "/" + filename
3334
} else {
34-
modePath = "files/external/" + module + ".txt"
35+
modePath = "files/external/" + filename
3536
}
3637

3738
modePrompt, err := fileLoader.loadPromptFileFromEmbed(modePath)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
name: CD
3+
description: CI/CD Pipeline Assistant
4+
result_description: CI/CD pipeline configuration
5+
module: CD
6+
---
7+
8+
***You are an opinionated DevOps expert.*** The user is a novice (e.g., a VP of Engineering or developer unfamiliar with DevOps), and they don't know what Harness can do. But ***you do***, because you have access to Harness documentation, templates, MCP context, and DevOps best practices.
9+
10+
***Your task is to guide them in building a CI/CD pipeline.***
11+
***Engage in a dialog*** to understand their goals—such as quality, security, resiliency, and compliance—and recommend a pipeline structure accordingly.
12+
13+
**Always use the minimum number of stages needed to achieve the user's requirements. Keep the initial pipeline as simple as possible.**
14+
15+
**Ask about their goals first, rather than directly asking about security or compliance steps.**
16+
17+
**Always ask one question at a time, waiting for their response before moving to the next.**
18+
19+
***Check for existing Harness entities*** (Secrets, Services, Environments, Connectors, Templates) using MCP APIs.
20+
21+
If multiple are found, ***show names and descriptions and ask the user to select***. If none exist, recommend creating them and include the request in the final DevOps Agent prompt.
22+
23+
When appropriate, ***ask if there are any compliance or policy requirements***, but only after discussing broader goals. If unsure, have the user describe them in English.
24+
25+
***Before generating YAML, summarize your plan and show the DevOps Agent prompt. Only proceed once the user confirms.***
File renamed without changes.

pkg/modules/registry.go

Lines changed: 92 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package modules
22

33
import (
4+
"encoding/json"
5+
"fmt"
46
"log/slog"
57
"strings"
68

@@ -101,57 +103,94 @@ func (r *ModuleRegistry) RegisterPrompts(mcpServer *server.MCPServer) error {
101103
}
102104

103105
func registerPrompts(moduleID string, cfg *config.Config, mcpServer *server.MCPServer) error {
104-
// Get module-specific prompts
105-
modulePrompts, err := prompts.GetModulePrompts(prompts.PromptFiles, strings.ToLower(moduleID), cfg.Internal)
106-
if err != nil {
107-
return err
108-
}
109-
110-
if len(modulePrompts) == 0 {
111-
// No prompts for this module
112-
return nil
113-
}
114-
115-
// Combine all prompt contents into a single prompt
116-
var combinedContent strings.Builder
117-
118-
// Use the first prompt's metadata for description and result description if available
119-
description := ""
120-
if len(modulePrompts) > 0 && modulePrompts[0].Metadata.Description != "" {
121-
description = modulePrompts[0].Metadata.Description
122-
}
123-
124-
resultDescription := ""
125-
if len(modulePrompts) > 0 && modulePrompts[0].Metadata.ResultDescription != "" {
126-
resultDescription = modulePrompts[0].Metadata.ResultDescription
127-
}
128-
129-
// Combine all prompt contents
130-
for i, promptFile := range modulePrompts {
131-
if promptFile.Content != "" {
132-
if i > 0 {
133-
combinedContent.WriteString("\n\n")
134-
}
135-
combinedContent.WriteString(promptFile.Content)
136-
}
137-
}
138-
139-
// Create a single MCP prompt with the module ID as the name
140-
mcpPrompt := p.NewPrompt().
141-
SetName(moduleID). // Use moduleID as the prompt name
142-
SetDescription(description).
143-
SetResultDescription(resultDescription).
144-
SetText(combinedContent.String()).
145-
Build()
146-
147-
// Create a Prompts collection with the single prompt
148-
mcpPrompts := p.Prompts{}
149-
mcpPrompts.Append(mcpPrompt)
150-
151-
slog.Info("Registering prompt for module", "module", moduleID, "contentLength", len(combinedContent.String()))
152-
153-
// Register the prompt with the MCP server
154-
p.AddPrompts(mcpPrompts, mcpServer)
155-
156-
return nil
106+
// Create a map to store prompts by mode
107+
modulePromptsByMode := map[string][]prompts.PromptFile{
108+
string(p.Standard): {},
109+
string(p.Architect): {},
110+
}
111+
112+
// Get module-specific prompts for standard mode
113+
standardPrompts, err := prompts.GetModulePrompts(prompts.PromptFiles, strings.ToLower(moduleID), cfg.Internal, string(p.Standard))
114+
if err != nil {
115+
return fmt.Errorf("failed to get standard prompts for module %s: %v", moduleID, err)
116+
}
117+
modulePromptsByMode[string(p.Standard)] = standardPrompts
118+
119+
// Get module-specific prompts for architect mode (only for internal)
120+
if cfg.Internal {
121+
architectPrompts, err := prompts.GetModulePrompts(prompts.PromptFiles, strings.ToLower(moduleID), cfg.Internal, string(p.Architect))
122+
if err != nil {
123+
return fmt.Errorf("failed to get architect prompts for module %s: %v", moduleID, err)
124+
}
125+
modulePromptsByMode[string(p.Architect)] = architectPrompts
126+
}
127+
128+
// Check if we have any prompts for this module
129+
totalPrompts := len(modulePromptsByMode[string(p.Standard)]) + len(modulePromptsByMode[string(p.Architect)])
130+
if totalPrompts == 0 {
131+
// No prompts for this module
132+
return nil
133+
}
134+
135+
// Create a map to store combined content for each mode
136+
modeContents := make(map[string]string)
137+
138+
// Get description and result description from the first available prompt
139+
description := ""
140+
resultDescription := ""
141+
142+
// Process each mode separately to build the content map
143+
for mode, modePrompts := range modulePromptsByMode {
144+
if len(modePrompts) == 0 {
145+
continue // Skip empty modes
146+
}
147+
148+
// Use the first prompt's metadata for description and result description if not already set
149+
if description == "" && modePrompts[0].Metadata.Description != "" {
150+
description = modePrompts[0].Metadata.Description
151+
}
152+
153+
if resultDescription == "" && modePrompts[0].Metadata.ResultDescription != "" {
154+
resultDescription = modePrompts[0].Metadata.ResultDescription
155+
}
156+
157+
// Combine all prompt contents for this mode
158+
var combinedContent strings.Builder
159+
for i, promptFile := range modePrompts {
160+
if promptFile.Content != "" {
161+
if i > 0 {
162+
combinedContent.WriteString("\n\n")
163+
}
164+
combinedContent.WriteString(promptFile.Content)
165+
}
166+
}
167+
168+
// Store the combined content for this mode
169+
modeContents[mode] = combinedContent.String()
170+
}
171+
172+
// Convert the mode contents map to JSON
173+
contentJSON, err := json.Marshal(modeContents)
174+
if err != nil {
175+
return fmt.Errorf("failed to marshal mode contents: %v", err)
176+
}
177+
178+
// Create a single MCP prompt with the module ID as the name
179+
mcpPrompt := p.NewPrompt().
180+
SetName(strings.ToUpper(moduleID)). // Use moduleID as the prompt name
181+
SetDescription(description).
182+
SetResultDescription(resultDescription).
183+
SetText(string(contentJSON)). // Store the JSON map as the prompt text
184+
Build()
185+
186+
// Create a Prompts collection with the single prompt
187+
mcpPrompts := p.Prompts{}
188+
mcpPrompts.Append(mcpPrompt)
189+
190+
slog.Info("Registering prompt for", "module", moduleID, "contentLength", len(contentJSON))
191+
192+
// Register the prompt with the MCP server
193+
p.AddPrompts(mcpPrompts, mcpServer)
194+
195+
return nil
157196
}

pkg/prompts/promptsregistry.go

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package prompts
22

33
import (
44
"context"
5+
"encoding/json"
6+
"fmt"
57
"log/slog"
68
"github.com/mark3labs/mcp-go/mcp"
79
"github.com/mark3labs/mcp-go/server"
@@ -13,11 +15,18 @@ import (
1315
// Role represents the role of the prompt creator, either User or Assistant.
1416
type Role int
1517

18+
type Mode string
19+
1620
const (
1721
User Role = iota // 0
1822
Assistant // 1
1923
)
2024

25+
const (
26+
Standard Mode = "standard"
27+
Architect Mode = "architect"
28+
)
29+
2130
// Prompt represents the prompt data needed to add to the MCP server.
2231
type Prompt struct {
2332
Name string
@@ -93,18 +102,44 @@ func AddPrompts(prompts Prompts, mcpServer *server.MCPServer) {
93102
}
94103
}
95104

96-
// createPrompt converts a Prompt into an MCP prompt and defines its handler function.
97105
func createPrompt(prompt *Prompt) (mcp.Prompt, server.PromptHandlerFunc) {
98106
role := mcp.RoleUser
99107
if prompt.Role == Assistant {
100108
role = mcp.RoleAssistant
101109
}
102110

103-
return mcp.NewPrompt(prompt.Name, mcp.WithPromptDescription(prompt.Description)),
111+
return mcp.NewPrompt(
112+
prompt.Name,
113+
mcp.WithPromptDescription(prompt.Description),
114+
mcp.WithArgument("mode", mcp.ArgumentDescription("Selects the prompt mode: 'standard' or 'architect'")),
115+
),
104116
func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
105-
return mcp.NewGetPromptResult(
106-
prompt.ResultDescription,
107-
[]mcp.PromptMessage{mcp.NewPromptMessage(role, mcp.NewTextContent(prompt.Text))},
108-
), nil
117+
// Determine mode (default to "standard") and return different prompt text accordingly.
118+
mode := string(Standard) // Default to standard mode
119+
if v, ok := request.Params.Arguments["mode"]; ok && v != "" {
120+
mode = v
121+
}
122+
123+
// Parse the JSON-encoded map of prompt contents
124+
var modeContents map[string]string
125+
if err := json.Unmarshal([]byte(prompt.Text), &modeContents); err != nil {
126+
slog.Error("Failed to parse prompt content JSON", "error", err, "promptName", prompt.Name)
127+
return nil, err
128+
}
129+
130+
// Get the content for the requested mode, fallback to standard if not found
131+
text, ok := modeContents[mode]
132+
if !ok {
133+
// If the requested mode is not available, fall back to standard mode
134+
text, ok = modeContents[string(Standard)]
135+
if !ok {
136+
return nil, fmt.Errorf("prompt mode %s not found for prompt %s", mode, prompt.Name)
137+
}
138+
}
139+
140+
return mcp.NewGetPromptResult(
141+
prompt.ResultDescription,
142+
[]mcp.PromptMessage{mcp.NewPromptMessage(role, mcp.NewTextContent(text))},
143+
), nil
109144
}
110-
}
145+
}

0 commit comments

Comments
 (0)