feat(agentic): support brief-driven content generation

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 10:54:49 +00:00
parent 8f8e0f09ca
commit 3d528e6963
4 changed files with 100 additions and 10 deletions

View file

@ -154,13 +154,17 @@ func (s *PrepSubsystem) cmdPrep(options core.Options) core.Result {
func (s *PrepSubsystem) cmdGenerate(options core.Options) core.Result {
prompt := optionStringValue(options, "prompt", "_arg")
if prompt == "" {
core.Print(nil, "usage: core-agent generate --prompt=\"Draft a release note\" [--provider=claude] [--config='{\"max_tokens\":4000}']")
return core.Result{Value: core.E("agentic.cmdGenerate", "prompt is required", nil), OK: false}
briefID := optionStringValue(options, "brief_id", "brief-id")
template := optionStringValue(options, "template")
if prompt == "" && (briefID == "" || template == "") {
core.Print(nil, "usage: core-agent generate --prompt=\"Draft a release note\" [--brief-id=brief_1 --template=help-article] [--provider=claude] [--config='{\"max_tokens\":4000}']")
return core.Result{Value: core.E("agentic.cmdGenerate", "prompt or brief-id/template is required", nil), OK: false}
}
result := s.handleContentGenerate(s.commandContext(), core.NewOptions(
core.Option{Key: "prompt", Value: prompt},
core.Option{Key: "brief_id", Value: briefID},
core.Option{Key: "template", Value: template},
core.Option{Key: "provider", Value: options.String("provider")},
core.Option{Key: "config", Value: options.String("config")},
))

View file

@ -14,6 +14,7 @@ import (
core "dappco.re/go/core"
"dappco.re/go/core/forge"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// testPrepWithCore creates a PrepSubsystem backed by a real Core + Forge mock.
@ -719,6 +720,40 @@ func TestCommands_CmdGenerate_Good(t *testing.T) {
assert.Contains(t, output, "content: Release notes draft")
}
func TestCommands_CmdGenerate_Good_BriefTemplate(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/v1/content/generate", r.URL.Path)
assert.Equal(t, http.MethodPost, r.Method)
bodyResult := core.ReadAll(r.Body)
require.True(t, bodyResult.OK)
var payload map[string]any
parseResult := core.JSONUnmarshalString(bodyResult.Value.(string), &payload)
require.True(t, parseResult.OK)
assert.Equal(t, "brief_1", payload["brief_id"])
assert.Equal(t, "help-article", payload["template"])
_, _ = w.Write([]byte(`{"data":{"id":"gen_2","provider":"claude","model":"claude-3.7-sonnet","content":"Template draft","status":"completed"}}`))
}))
defer server.Close()
s := testPrepWithPlatformServer(t, server, "secret-token")
output := captureStdout(t, func() {
r := s.cmdGenerate(core.NewOptions(
core.Option{Key: "brief_id", Value: "brief_1"},
core.Option{Key: "template", Value: "help-article"},
core.Option{Key: "provider", Value: "claude"},
))
assert.True(t, r.OK)
})
assert.Contains(t, output, "provider: claude")
assert.Contains(t, output, "model: claude-3.7-sonnet")
assert.Contains(t, output, "status: completed")
assert.Contains(t, output, "content: Template draft")
}
func TestCommands_CmdExtract_Good(t *testing.T) {
s, _ := testPrepWithCore(t, nil)
target := core.JoinPath(t.TempDir(), "extract-test")

View file

@ -41,9 +41,15 @@ type ContentBrief struct {
UpdatedAt string `json:"updated_at,omitempty"`
}
// input := agentic.ContentGenerateInput{Prompt: "Draft a release note", Provider: "claude"}
// input := agentic.ContentGenerateInput{
// BriefID: "brief_1",
// Template: "help-article",
// Provider: "claude",
// }
type ContentGenerateInput struct {
Prompt string `json:"prompt"`
Prompt string `json:"prompt,omitempty"`
BriefID string `json:"brief_id,omitempty"`
Template string `json:"template,omitempty"`
Provider string `json:"provider,omitempty"`
Config map[string]any `json:"config,omitempty"`
}
@ -151,6 +157,8 @@ type ContentFromPlanOutput struct {
func (s *PrepSubsystem) handleContentGenerate(ctx context.Context, options core.Options) core.Result {
_, output, err := s.contentGenerate(ctx, nil, ContentGenerateInput{
Prompt: optionStringValue(options, "prompt"),
BriefID: optionStringValue(options, "brief_id", "brief-id"),
Template: optionStringValue(options, "template"),
Provider: optionStringValue(options, "provider"),
Config: optionAnyMapValue(options, "config"),
})
@ -261,7 +269,7 @@ func (s *PrepSubsystem) handleContentFromPlan(ctx context.Context, options core.
func (s *PrepSubsystem) registerContentTools(server *mcp.Server) {
mcp.AddTool(server, &mcp.Tool{
Name: "content_generate",
Description: "Generate content using the platform AI provider abstraction.",
Description: "Generate content from a prompt or a brief/template pair using the platform AI provider abstraction.",
}, s.contentGenerate)
mcp.AddTool(server, &mcp.Tool{
@ -306,12 +314,22 @@ func (s *PrepSubsystem) registerContentTools(server *mcp.Server) {
}
func (s *PrepSubsystem) contentGenerate(ctx context.Context, _ *mcp.CallToolRequest, input ContentGenerateInput) (*mcp.CallToolResult, ContentGenerateOutput, error) {
if core.Trim(input.Prompt) == "" {
return nil, ContentGenerateOutput{}, core.E("contentGenerate", "prompt is required", nil)
hasPrompt := core.Trim(input.Prompt) != ""
hasBrief := core.Trim(input.BriefID) != ""
hasTemplate := core.Trim(input.Template) != ""
if !hasPrompt && !(hasBrief && hasTemplate) {
return nil, ContentGenerateOutput{}, core.E("contentGenerate", "prompt or brief_id plus template is required", nil)
}
body := map[string]any{
"prompt": input.Prompt,
body := map[string]any{}
if hasPrompt {
body["prompt"] = input.Prompt
}
if input.BriefID != "" {
body["brief_id"] = input.BriefID
}
if input.Template != "" {
body["template"] = input.Template
}
if input.Provider != "" {
body["provider"] = input.Provider

View file

@ -53,6 +53,39 @@ func TestContent_HandleContentGenerate_Good(t *testing.T) {
assert.Equal(t, 48, output.Result.OutputTokens)
}
func TestContent_HandleContentGenerate_Good_BriefTemplate(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/v1/content/generate", r.URL.Path)
require.Equal(t, http.MethodPost, r.Method)
bodyResult := core.ReadAll(r.Body)
require.True(t, bodyResult.OK)
var payload map[string]any
parseResult := core.JSONUnmarshalString(bodyResult.Value.(string), &payload)
require.True(t, parseResult.OK)
require.Equal(t, "brief_1", payload["brief_id"])
require.Equal(t, "help-article", payload["template"])
require.NotContains(t, payload, "prompt")
_, _ = w.Write([]byte(`{"data":{"result":{"id":"gen_2","provider":"claude","model":"claude-3.7-sonnet","content":"Template draft","status":"completed"}}}`))
}))
defer server.Close()
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
result := subsystem.handleContentGenerate(context.Background(), core.NewOptions(
core.Option{Key: "brief_id", Value: "brief_1"},
core.Option{Key: "template", Value: "help-article"},
core.Option{Key: "provider", Value: "claude"},
))
require.True(t, result.OK)
output, ok := result.Value.(ContentGenerateOutput)
require.True(t, ok)
assert.Equal(t, "gen_2", output.Result.ID)
assert.Equal(t, "Template draft", output.Result.Content)
}
func TestContent_HandleContentGenerate_Bad(t *testing.T) {
subsystem := testPrepWithPlatformServer(t, nil, "secret-token")