feat(agentic): add content platform compatibility surfaces
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
8ed911eb27
commit
b84e5692a2
5 changed files with 839 additions and 0 deletions
580
pkg/agentic/content.go
Normal file
580
pkg/agentic/content.go
Normal file
|
|
@ -0,0 +1,580 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package agentic
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// result := agentic.ContentResult{Provider: "claude", Model: "claude-3.7-sonnet", Content: "Draft ready"}
|
||||
type ContentResult struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
BatchID string `json:"batch_id,omitempty"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
InputTokens int `json:"input_tokens,omitempty"`
|
||||
OutputTokens int `json:"output_tokens,omitempty"`
|
||||
DurationMS int `json:"duration_ms,omitempty"`
|
||||
StopReason string `json:"stop_reason,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
Raw map[string]any `json:"raw,omitempty"`
|
||||
}
|
||||
|
||||
// brief := agentic.ContentBrief{ID: "brief_1", Slug: "host-link", Title: "LinkHost", Category: "product"}
|
||||
type ContentBrief struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Product string `json:"product,omitempty"`
|
||||
Category string `json:"category,omitempty"`
|
||||
Brief string `json:"brief,omitempty"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
UpdatedAt string `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
// input := agentic.ContentGenerateInput{Prompt: "Draft a release note", Provider: "claude"}
|
||||
type ContentGenerateInput struct {
|
||||
Prompt string `json:"prompt"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Config map[string]any `json:"config,omitempty"`
|
||||
}
|
||||
|
||||
// input := agentic.ContentBatchGenerateInput{BatchID: "batch_123", Provider: "gemini"}
|
||||
type ContentBatchGenerateInput struct {
|
||||
BatchID string `json:"batch_id"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
DryRun bool `json:"dry_run,omitempty"`
|
||||
}
|
||||
|
||||
// input := agentic.ContentBriefCreateInput{Title: "LinkHost brief", Product: "LinkHost"}
|
||||
type ContentBriefCreateInput struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Product string `json:"product,omitempty"`
|
||||
Category string `json:"category,omitempty"`
|
||||
Brief string `json:"brief,omitempty"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
Context map[string]any `json:"context,omitempty"`
|
||||
Payload map[string]any `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
// input := agentic.ContentBriefGetInput{BriefID: "host-link"}
|
||||
type ContentBriefGetInput struct {
|
||||
BriefID string `json:"brief_id"`
|
||||
}
|
||||
|
||||
// input := agentic.ContentBriefListInput{Category: "product", Limit: 10}
|
||||
type ContentBriefListInput struct {
|
||||
Category string `json:"category,omitempty"`
|
||||
Product string `json:"product,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
}
|
||||
|
||||
// input := agentic.ContentStatusInput{BatchID: "batch_123"}
|
||||
type ContentStatusInput struct {
|
||||
BatchID string `json:"batch_id"`
|
||||
}
|
||||
|
||||
// input := agentic.ContentUsageStatsInput{Provider: "claude", Period: "week"}
|
||||
type ContentUsageStatsInput struct {
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Period string `json:"period,omitempty"`
|
||||
Since string `json:"since,omitempty"`
|
||||
Until string `json:"until,omitempty"`
|
||||
}
|
||||
|
||||
// input := agentic.ContentFromPlanInput{PlanSlug: "release-notes", Provider: "openai"}
|
||||
type ContentFromPlanInput struct {
|
||||
PlanSlug string `json:"plan_slug"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
Template string `json:"template,omitempty"`
|
||||
Config map[string]any `json:"config,omitempty"`
|
||||
Payload map[string]any `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
// out := agentic.ContentGenerateOutput{Success: true, Result: agentic.ContentResult{Provider: "claude"}}
|
||||
type ContentGenerateOutput struct {
|
||||
Success bool `json:"success"`
|
||||
Result ContentResult `json:"result"`
|
||||
}
|
||||
|
||||
// out := agentic.ContentBatchOutput{Success: true, Batch: map[string]any{"batch_id": "batch_123"}}
|
||||
type ContentBatchOutput struct {
|
||||
Success bool `json:"success"`
|
||||
Batch map[string]any `json:"batch"`
|
||||
}
|
||||
|
||||
// out := agentic.ContentBriefOutput{Success: true, Brief: agentic.ContentBrief{Slug: "host-link"}}
|
||||
type ContentBriefOutput struct {
|
||||
Success bool `json:"success"`
|
||||
Brief ContentBrief `json:"brief"`
|
||||
}
|
||||
|
||||
// out := agentic.ContentBriefListOutput{Success: true, Total: 1, Briefs: []agentic.ContentBrief{{Slug: "host-link"}}}
|
||||
type ContentBriefListOutput struct {
|
||||
Success bool `json:"success"`
|
||||
Total int `json:"total"`
|
||||
Briefs []ContentBrief `json:"briefs"`
|
||||
}
|
||||
|
||||
// out := agentic.ContentStatusOutput{Success: true, Status: map[string]any{"status": "running"}}
|
||||
type ContentStatusOutput struct {
|
||||
Success bool `json:"success"`
|
||||
Status map[string]any `json:"status"`
|
||||
}
|
||||
|
||||
// out := agentic.ContentUsageStatsOutput{Success: true, Usage: map[string]any{"calls": 4}}
|
||||
type ContentUsageStatsOutput struct {
|
||||
Success bool `json:"success"`
|
||||
Usage map[string]any `json:"usage"`
|
||||
}
|
||||
|
||||
// out := agentic.ContentFromPlanOutput{Success: true, Result: agentic.ContentResult{BatchID: "batch_123"}}
|
||||
type ContentFromPlanOutput struct {
|
||||
Success bool `json:"success"`
|
||||
Result ContentResult `json:"result"`
|
||||
}
|
||||
|
||||
// result := c.Action("content.generate").Run(ctx, core.NewOptions(core.Option{Key: "prompt", Value: "Draft a release note"}))
|
||||
func (s *PrepSubsystem) handleContentGenerate(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.contentGenerate(ctx, nil, ContentGenerateInput{
|
||||
Prompt: optionStringValue(options, "prompt"),
|
||||
Provider: optionStringValue(options, "provider"),
|
||||
Config: optionAnyMapValue(options, "config"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
// result := c.Action("content.batch.generate").Run(ctx, core.NewOptions(core.Option{Key: "batch_id", Value: "batch_123"}))
|
||||
func (s *PrepSubsystem) handleContentBatchGenerate(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.contentBatchGenerate(ctx, nil, ContentBatchGenerateInput{
|
||||
BatchID: optionStringValue(options, "batch_id", "batch-id", "_arg"),
|
||||
Provider: optionStringValue(options, "provider"),
|
||||
DryRun: optionBoolValue(options, "dry_run", "dry-run"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
// result := c.Action("content.brief.create").Run(ctx, core.NewOptions(core.Option{Key: "title", Value: "LinkHost brief"}))
|
||||
func (s *PrepSubsystem) handleContentBriefCreate(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.contentBriefCreate(ctx, nil, ContentBriefCreateInput{
|
||||
Title: optionStringValue(options, "title"),
|
||||
Name: optionStringValue(options, "name"),
|
||||
Slug: optionStringValue(options, "slug"),
|
||||
Product: optionStringValue(options, "product"),
|
||||
Category: optionStringValue(options, "category"),
|
||||
Brief: optionStringValue(options, "brief"),
|
||||
Summary: optionStringValue(options, "summary"),
|
||||
Metadata: optionAnyMapValue(options, "metadata"),
|
||||
Context: optionAnyMapValue(options, "context"),
|
||||
Payload: optionAnyMapValue(options, "payload"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
// result := c.Action("content.brief.get").Run(ctx, core.NewOptions(core.Option{Key: "brief_id", Value: "host-link"}))
|
||||
func (s *PrepSubsystem) handleContentBriefGet(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.contentBriefGet(ctx, nil, ContentBriefGetInput{
|
||||
BriefID: optionStringValue(options, "brief_id", "brief-id", "id", "slug", "_arg"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
// result := c.Action("content.brief.list").Run(ctx, core.NewOptions(core.Option{Key: "category", Value: "product"}))
|
||||
func (s *PrepSubsystem) handleContentBriefList(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.contentBriefList(ctx, nil, ContentBriefListInput{
|
||||
Category: optionStringValue(options, "category"),
|
||||
Product: optionStringValue(options, "product"),
|
||||
Limit: optionIntValue(options, "limit"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
// result := c.Action("content.status").Run(ctx, core.NewOptions(core.Option{Key: "batch_id", Value: "batch_123"}))
|
||||
func (s *PrepSubsystem) handleContentStatus(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.contentStatus(ctx, nil, ContentStatusInput{
|
||||
BatchID: optionStringValue(options, "batch_id", "batch-id", "_arg"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
// result := c.Action("content.usage.stats").Run(ctx, core.NewOptions(core.Option{Key: "provider", Value: "claude"}))
|
||||
func (s *PrepSubsystem) handleContentUsageStats(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.contentUsageStats(ctx, nil, ContentUsageStatsInput{
|
||||
Provider: optionStringValue(options, "provider"),
|
||||
Period: optionStringValue(options, "period"),
|
||||
Since: optionStringValue(options, "since"),
|
||||
Until: optionStringValue(options, "until"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
// result := c.Action("content.from.plan").Run(ctx, core.NewOptions(core.Option{Key: "plan_slug", Value: "release-notes"}))
|
||||
func (s *PrepSubsystem) handleContentFromPlan(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.contentFromPlan(ctx, nil, ContentFromPlanInput{
|
||||
PlanSlug: optionStringValue(options, "plan_slug", "plan", "slug", "_arg"),
|
||||
Provider: optionStringValue(options, "provider"),
|
||||
Prompt: optionStringValue(options, "prompt"),
|
||||
Template: optionStringValue(options, "template"),
|
||||
Config: optionAnyMapValue(options, "config"),
|
||||
Payload: optionAnyMapValue(options, "payload"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) registerContentTools(server *mcp.Server) {
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "content_generate",
|
||||
Description: "Generate content using the platform AI provider abstraction.",
|
||||
}, s.contentGenerate)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "content_batch_generate",
|
||||
Description: "Generate content for a stored batch specification.",
|
||||
}, s.contentBatchGenerate)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "content_brief_create",
|
||||
Description: "Create a reusable content brief for later generation work.",
|
||||
}, s.contentBriefCreate)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "content_brief_get",
|
||||
Description: "Read a reusable content brief by ID or slug.",
|
||||
}, s.contentBriefGet)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "content_brief_list",
|
||||
Description: "List reusable content briefs with optional category and product filters.",
|
||||
}, s.contentBriefList)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "content_status",
|
||||
Description: "Read batch content generation status by batch ID.",
|
||||
}, s.contentStatus)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "content_usage_stats",
|
||||
Description: "Read AI usage statistics for the content pipeline.",
|
||||
}, s.contentUsageStats)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "content_from_plan",
|
||||
Description: "Generate content using stored plan context and an optional provider override.",
|
||||
}, s.contentFromPlan)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
body := map[string]any{
|
||||
"prompt": input.Prompt,
|
||||
}
|
||||
if input.Provider != "" {
|
||||
body["provider"] = input.Provider
|
||||
}
|
||||
if len(input.Config) > 0 {
|
||||
body["config"] = input.Config
|
||||
}
|
||||
|
||||
result := s.platformPayload(ctx, "content.generate", "POST", "/v1/content/generate", body)
|
||||
if !result.OK {
|
||||
return nil, ContentGenerateOutput{}, resultErrorValue("content.generate", result)
|
||||
}
|
||||
|
||||
return nil, ContentGenerateOutput{
|
||||
Success: true,
|
||||
Result: parseContentResult(payloadResourceMap(result.Value.(map[string]any), "result", "content", "generation")),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) contentBatchGenerate(ctx context.Context, _ *mcp.CallToolRequest, input ContentBatchGenerateInput) (*mcp.CallToolResult, ContentBatchOutput, error) {
|
||||
if core.Trim(input.BatchID) == "" {
|
||||
return nil, ContentBatchOutput{}, core.E("contentBatchGenerate", "batch_id is required", nil)
|
||||
}
|
||||
|
||||
body := map[string]any{
|
||||
"batch_id": input.BatchID,
|
||||
}
|
||||
if input.Provider != "" {
|
||||
body["provider"] = input.Provider
|
||||
}
|
||||
if input.DryRun {
|
||||
body["dry_run"] = true
|
||||
}
|
||||
|
||||
result := s.platformPayload(ctx, "content.batch.generate", "POST", "/v1/content/batch/generate", body)
|
||||
if !result.OK {
|
||||
return nil, ContentBatchOutput{}, resultErrorValue("content.batch.generate", result)
|
||||
}
|
||||
|
||||
return nil, ContentBatchOutput{
|
||||
Success: true,
|
||||
Batch: payloadResourceMap(result.Value.(map[string]any), "batch", "result", "status"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) contentBriefCreate(ctx context.Context, _ *mcp.CallToolRequest, input ContentBriefCreateInput) (*mcp.CallToolResult, ContentBriefOutput, error) {
|
||||
body := map[string]any{}
|
||||
if input.Title != "" {
|
||||
body["title"] = input.Title
|
||||
}
|
||||
if input.Name != "" {
|
||||
body["name"] = input.Name
|
||||
}
|
||||
if input.Slug != "" {
|
||||
body["slug"] = input.Slug
|
||||
}
|
||||
if input.Product != "" {
|
||||
body["product"] = input.Product
|
||||
}
|
||||
if input.Category != "" {
|
||||
body["category"] = input.Category
|
||||
}
|
||||
if input.Brief != "" {
|
||||
body["brief"] = input.Brief
|
||||
}
|
||||
if input.Summary != "" {
|
||||
body["summary"] = input.Summary
|
||||
}
|
||||
if len(input.Metadata) > 0 {
|
||||
body["metadata"] = input.Metadata
|
||||
}
|
||||
if len(input.Context) > 0 {
|
||||
body["context"] = input.Context
|
||||
}
|
||||
body = mergeContentPayload(body, input.Payload)
|
||||
if len(body) == 0 {
|
||||
return nil, ContentBriefOutput{}, core.E("contentBriefCreate", "content brief data is required", nil)
|
||||
}
|
||||
|
||||
result := s.platformPayload(ctx, "content.brief.create", "POST", "/v1/content/briefs", body)
|
||||
if !result.OK {
|
||||
return nil, ContentBriefOutput{}, resultErrorValue("content.brief.create", result)
|
||||
}
|
||||
|
||||
return nil, ContentBriefOutput{
|
||||
Success: true,
|
||||
Brief: parseContentBrief(payloadResourceMap(result.Value.(map[string]any), "brief", "item")),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) contentBriefGet(ctx context.Context, _ *mcp.CallToolRequest, input ContentBriefGetInput) (*mcp.CallToolResult, ContentBriefOutput, error) {
|
||||
if core.Trim(input.BriefID) == "" {
|
||||
return nil, ContentBriefOutput{}, core.E("contentBriefGet", "brief_id is required", nil)
|
||||
}
|
||||
|
||||
result := s.platformPayload(ctx, "content.brief.get", "GET", core.Concat("/v1/content/briefs/", input.BriefID), nil)
|
||||
if !result.OK {
|
||||
return nil, ContentBriefOutput{}, resultErrorValue("content.brief.get", result)
|
||||
}
|
||||
|
||||
return nil, ContentBriefOutput{
|
||||
Success: true,
|
||||
Brief: parseContentBrief(payloadResourceMap(result.Value.(map[string]any), "brief", "item")),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) contentBriefList(ctx context.Context, _ *mcp.CallToolRequest, input ContentBriefListInput) (*mcp.CallToolResult, ContentBriefListOutput, error) {
|
||||
path := "/v1/content/briefs"
|
||||
path = appendQueryParam(path, "category", input.Category)
|
||||
path = appendQueryParam(path, "product", input.Product)
|
||||
if input.Limit > 0 {
|
||||
path = appendQueryParam(path, "limit", core.Sprint(input.Limit))
|
||||
}
|
||||
|
||||
result := s.platformPayload(ctx, "content.brief.list", "GET", path, nil)
|
||||
if !result.OK {
|
||||
return nil, ContentBriefListOutput{}, resultErrorValue("content.brief.list", result)
|
||||
}
|
||||
|
||||
return nil, parseContentBriefListOutput(result.Value.(map[string]any)), nil
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) contentStatus(ctx context.Context, _ *mcp.CallToolRequest, input ContentStatusInput) (*mcp.CallToolResult, ContentStatusOutput, error) {
|
||||
if core.Trim(input.BatchID) == "" {
|
||||
return nil, ContentStatusOutput{}, core.E("contentStatus", "batch_id is required", nil)
|
||||
}
|
||||
|
||||
result := s.platformPayload(ctx, "content.status", "GET", core.Concat("/v1/content/status/", input.BatchID), nil)
|
||||
if !result.OK {
|
||||
return nil, ContentStatusOutput{}, resultErrorValue("content.status", result)
|
||||
}
|
||||
|
||||
return nil, ContentStatusOutput{
|
||||
Success: true,
|
||||
Status: payloadResourceMap(result.Value.(map[string]any), "status", "batch"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) contentUsageStats(ctx context.Context, _ *mcp.CallToolRequest, input ContentUsageStatsInput) (*mcp.CallToolResult, ContentUsageStatsOutput, error) {
|
||||
path := "/v1/content/usage/stats"
|
||||
path = appendQueryParam(path, "provider", input.Provider)
|
||||
path = appendQueryParam(path, "period", input.Period)
|
||||
path = appendQueryParam(path, "since", input.Since)
|
||||
path = appendQueryParam(path, "until", input.Until)
|
||||
|
||||
result := s.platformPayload(ctx, "content.usage.stats", "GET", path, nil)
|
||||
if !result.OK {
|
||||
return nil, ContentUsageStatsOutput{}, resultErrorValue("content.usage.stats", result)
|
||||
}
|
||||
|
||||
return nil, ContentUsageStatsOutput{
|
||||
Success: true,
|
||||
Usage: payloadResourceMap(result.Value.(map[string]any), "usage", "stats"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) contentFromPlan(ctx context.Context, _ *mcp.CallToolRequest, input ContentFromPlanInput) (*mcp.CallToolResult, ContentFromPlanOutput, error) {
|
||||
if core.Trim(input.PlanSlug) == "" {
|
||||
return nil, ContentFromPlanOutput{}, core.E("contentFromPlan", "plan_slug is required", nil)
|
||||
}
|
||||
|
||||
body := map[string]any{
|
||||
"plan_slug": input.PlanSlug,
|
||||
}
|
||||
if input.Provider != "" {
|
||||
body["provider"] = input.Provider
|
||||
}
|
||||
if input.Prompt != "" {
|
||||
body["prompt"] = input.Prompt
|
||||
}
|
||||
if input.Template != "" {
|
||||
body["template"] = input.Template
|
||||
}
|
||||
if len(input.Config) > 0 {
|
||||
body["config"] = input.Config
|
||||
}
|
||||
body = mergeContentPayload(body, input.Payload)
|
||||
|
||||
result := s.platformPayload(ctx, "content.from.plan", "POST", "/v1/content/from-plan", body)
|
||||
if !result.OK {
|
||||
return nil, ContentFromPlanOutput{}, resultErrorValue("content.from.plan", result)
|
||||
}
|
||||
|
||||
return nil, ContentFromPlanOutput{
|
||||
Success: true,
|
||||
Result: parseContentResult(payloadResourceMap(result.Value.(map[string]any), "result", "content", "generation")),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mergeContentPayload(target, extra map[string]any) map[string]any {
|
||||
if len(target) == 0 {
|
||||
target = map[string]any{}
|
||||
}
|
||||
for key, value := range extra {
|
||||
if value != nil {
|
||||
target[key] = value
|
||||
}
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
func parseContentResult(values map[string]any) ContentResult {
|
||||
result := ContentResult{
|
||||
ID: contentMapStringValue(values, "id"),
|
||||
BatchID: contentMapStringValue(values, "batch_id", "batchId"),
|
||||
Prompt: contentMapStringValue(values, "prompt"),
|
||||
Provider: contentMapStringValue(values, "provider"),
|
||||
Model: contentMapStringValue(values, "model", "model_name"),
|
||||
Content: contentMapStringValue(values, "content", "text", "output"),
|
||||
Status: contentMapStringValue(values, "status"),
|
||||
InputTokens: mapIntValue(values, "input_tokens", "inputTokens"),
|
||||
OutputTokens: mapIntValue(values, "output_tokens", "outputTokens"),
|
||||
DurationMS: mapIntValue(values, "duration_ms", "durationMs", "duration"),
|
||||
StopReason: contentMapStringValue(values, "stop_reason", "stopReason"),
|
||||
Metadata: anyMapValue(values["metadata"]),
|
||||
Raw: anyMapValue(values["raw"]),
|
||||
}
|
||||
if len(result.Raw) == 0 && len(values) > 0 {
|
||||
result.Raw = values
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func parseContentBrief(values map[string]any) ContentBrief {
|
||||
return ContentBrief{
|
||||
ID: contentMapStringValue(values, "id"),
|
||||
Slug: contentMapStringValue(values, "slug"),
|
||||
Name: contentMapStringValue(values, "name"),
|
||||
Title: contentMapStringValue(values, "title"),
|
||||
Product: contentMapStringValue(values, "product"),
|
||||
Category: contentMapStringValue(values, "category"),
|
||||
Brief: contentMapStringValue(values, "brief", "content", "body"),
|
||||
Summary: contentMapStringValue(values, "summary", "description"),
|
||||
Metadata: anyMapValue(values["metadata"]),
|
||||
CreatedAt: contentMapStringValue(values, "created_at"),
|
||||
UpdatedAt: contentMapStringValue(values, "updated_at"),
|
||||
}
|
||||
}
|
||||
|
||||
func parseContentBriefListOutput(payload map[string]any) ContentBriefListOutput {
|
||||
values := payloadDataSlice(payload, "briefs", "items")
|
||||
briefs := make([]ContentBrief, 0, len(values))
|
||||
for _, value := range values {
|
||||
briefs = append(briefs, parseContentBrief(value))
|
||||
}
|
||||
|
||||
total := mapIntValue(payload, "total", "count")
|
||||
if total == 0 {
|
||||
total = mapIntValue(payloadDataMap(payload), "total", "count")
|
||||
}
|
||||
if total == 0 {
|
||||
total = len(briefs)
|
||||
}
|
||||
|
||||
return ContentBriefListOutput{
|
||||
Success: true,
|
||||
Total: total,
|
||||
Briefs: briefs,
|
||||
}
|
||||
}
|
||||
|
||||
func contentMapStringValue(values map[string]any, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if value, ok := values[key]; ok {
|
||||
if text := stringValue(value); text != "" {
|
||||
return text
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
18
pkg/agentic/content_example_test.go
Normal file
18
pkg/agentic/content_example_test.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package agentic
|
||||
|
||||
import "fmt"
|
||||
|
||||
func Example_parseContentResult() {
|
||||
result := parseContentResult(map[string]any{
|
||||
"batch_id": "batch_123",
|
||||
"provider": "claude",
|
||||
"model": "claude-3.7-sonnet",
|
||||
"content": "Draft ready",
|
||||
"output_tokens": 64,
|
||||
})
|
||||
|
||||
fmt.Println(result.BatchID, result.Provider, result.Model, result.OutputTokens)
|
||||
// Output: batch_123 claude claude-3.7-sonnet 64
|
||||
}
|
||||
204
pkg/agentic/content_test.go
Normal file
204
pkg/agentic/content_test.go
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package agentic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestContent_HandleContentGenerate_Good(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)
|
||||
require.Equal(t, "Bearer secret-token", r.Header.Get("Authorization"))
|
||||
|
||||
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, "Draft a release note", payload["prompt"])
|
||||
require.Equal(t, "claude", payload["provider"])
|
||||
|
||||
config, ok := payload["config"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, float64(4000), config["max_tokens"])
|
||||
|
||||
_, _ = w.Write([]byte(`{"data":{"id":"gen_1","provider":"claude","model":"claude-3.7-sonnet","content":"Release notes draft","input_tokens":12,"output_tokens":48,"duration_ms":321}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
|
||||
result := subsystem.handleContentGenerate(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "prompt", Value: "Draft a release note"},
|
||||
core.Option{Key: "provider", Value: "claude"},
|
||||
core.Option{Key: "config", Value: `{"max_tokens":4000}`},
|
||||
))
|
||||
require.True(t, result.OK)
|
||||
|
||||
output, ok := result.Value.(ContentGenerateOutput)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "gen_1", output.Result.ID)
|
||||
assert.Equal(t, "claude", output.Result.Provider)
|
||||
assert.Equal(t, "claude-3.7-sonnet", output.Result.Model)
|
||||
assert.Equal(t, "Release notes draft", output.Result.Content)
|
||||
assert.Equal(t, 48, output.Result.OutputTokens)
|
||||
}
|
||||
|
||||
func TestContent_HandleContentGenerate_Bad(t *testing.T) {
|
||||
subsystem := testPrepWithPlatformServer(t, nil, "secret-token")
|
||||
|
||||
result := subsystem.handleContentGenerate(context.Background(), core.NewOptions())
|
||||
assert.False(t, result.OK)
|
||||
}
|
||||
|
||||
func TestContent_HandleContentGenerate_Ugly(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"data":`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
|
||||
result := subsystem.handleContentGenerate(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "prompt", Value: "Draft a release note"},
|
||||
))
|
||||
assert.False(t, result.OK)
|
||||
}
|
||||
|
||||
func TestContent_HandleContentBriefCreate_Good(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/v1/content/briefs", 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, "LinkHost brief", payload["title"])
|
||||
require.Equal(t, "LinkHost", payload["product"])
|
||||
|
||||
_, _ = w.Write([]byte(`{"data":{"brief":{"id":"brief_1","slug":"host-link","title":"LinkHost brief","product":"LinkHost","category":"product","brief":"Core context"}}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
|
||||
result := subsystem.handleContentBriefCreate(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "title", Value: "LinkHost brief"},
|
||||
core.Option{Key: "product", Value: "LinkHost"},
|
||||
core.Option{Key: "category", Value: "product"},
|
||||
core.Option{Key: "brief", Value: "Core context"},
|
||||
))
|
||||
require.True(t, result.OK)
|
||||
|
||||
output, ok := result.Value.(ContentBriefOutput)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "brief_1", output.Brief.ID)
|
||||
assert.Equal(t, "host-link", output.Brief.Slug)
|
||||
assert.Equal(t, "LinkHost", output.Brief.Product)
|
||||
}
|
||||
|
||||
func TestContent_HandleContentBriefList_Good(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/v1/content/briefs", r.URL.Path)
|
||||
require.Equal(t, "product", r.URL.Query().Get("category"))
|
||||
require.Equal(t, "5", r.URL.Query().Get("limit"))
|
||||
_, _ = w.Write([]byte(`{"data":{"briefs":[{"id":"brief_1","slug":"host-link","title":"LinkHost brief","category":"product"}],"total":1}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
|
||||
result := subsystem.handleContentBriefList(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "category", Value: "product"},
|
||||
core.Option{Key: "limit", Value: 5},
|
||||
))
|
||||
require.True(t, result.OK)
|
||||
|
||||
output, ok := result.Value.(ContentBriefListOutput)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 1, output.Total)
|
||||
require.Len(t, output.Briefs, 1)
|
||||
assert.Equal(t, "host-link", output.Briefs[0].Slug)
|
||||
}
|
||||
|
||||
func TestContent_HandleContentStatus_Good(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/v1/content/status/batch_123", r.URL.Path)
|
||||
_, _ = w.Write([]byte(`{"data":{"status":"running","batch_id":"batch_123","queued":2}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
|
||||
result := subsystem.handleContentStatus(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "batch_id", Value: "batch_123"},
|
||||
))
|
||||
require.True(t, result.OK)
|
||||
|
||||
output, ok := result.Value.(ContentStatusOutput)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "running", stringValue(output.Status["status"]))
|
||||
assert.Equal(t, "batch_123", stringValue(output.Status["batch_id"]))
|
||||
}
|
||||
|
||||
func TestContent_HandleContentUsageStats_Good(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/v1/content/usage/stats", r.URL.Path)
|
||||
require.Equal(t, "claude", r.URL.Query().Get("provider"))
|
||||
require.Equal(t, "week", r.URL.Query().Get("period"))
|
||||
_, _ = w.Write([]byte(`{"data":{"calls":4,"tokens":1200}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
|
||||
result := subsystem.handleContentUsageStats(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "provider", Value: "claude"},
|
||||
core.Option{Key: "period", Value: "week"},
|
||||
))
|
||||
require.True(t, result.OK)
|
||||
|
||||
output, ok := result.Value.(ContentUsageStatsOutput)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 4, intValue(output.Usage["calls"]))
|
||||
assert.Equal(t, 1200, intValue(output.Usage["tokens"]))
|
||||
}
|
||||
|
||||
func TestContent_HandleContentFromPlan_Good(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/v1/content/from-plan", 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, "release-notes", payload["plan_slug"])
|
||||
require.Equal(t, "openai", payload["provider"])
|
||||
|
||||
_, _ = w.Write([]byte(`{"data":{"result":{"batch_id":"batch_123","provider":"openai","model":"gpt-5.4","content":"Plan-driven draft","status":"completed"}}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
|
||||
result := subsystem.handleContentFromPlan(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "plan_slug", Value: "release-notes"},
|
||||
core.Option{Key: "provider", Value: "openai"},
|
||||
))
|
||||
require.True(t, result.OK)
|
||||
|
||||
output, ok := result.Value.(ContentFromPlanOutput)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "batch_123", output.Result.BatchID)
|
||||
assert.Equal(t, "completed", output.Result.Status)
|
||||
assert.Equal(t, "Plan-driven draft", output.Result.Content)
|
||||
}
|
||||
|
|
@ -216,6 +216,20 @@ func (s *PrepSubsystem) OnStartup(ctx context.Context) core.Result {
|
|||
c.Action("sprint.list", s.handleSprintList).Description = "List tracked platform sprints with optional filters"
|
||||
c.Action("sprint.update", s.handleSprintUpdate).Description = "Update a tracked platform sprint by slug"
|
||||
c.Action("sprint.archive", s.handleSprintArchive).Description = "Archive a tracked platform sprint by slug"
|
||||
c.Action("content.generate", s.handleContentGenerate).Description = "Generate content using the platform content pipeline"
|
||||
c.Action("content.batch.generate", s.handleContentBatchGenerate).Description = "Start or continue batch content generation"
|
||||
c.Action("content.batch_generate", s.handleContentBatchGenerate).Description = "Start or continue batch content generation"
|
||||
c.Action("content.brief.create", s.handleContentBriefCreate).Description = "Create a reusable content brief"
|
||||
c.Action("content.brief_create", s.handleContentBriefCreate).Description = "Create a reusable content brief"
|
||||
c.Action("content.brief.get", s.handleContentBriefGet).Description = "Read a content brief by ID or slug"
|
||||
c.Action("content.brief_get", s.handleContentBriefGet).Description = "Read a content brief by ID or slug"
|
||||
c.Action("content.brief.list", s.handleContentBriefList).Description = "List content briefs with optional filters"
|
||||
c.Action("content.brief_list", s.handleContentBriefList).Description = "List content briefs with optional filters"
|
||||
c.Action("content.status", s.handleContentStatus).Description = "Read batch content generation status"
|
||||
c.Action("content.usage.stats", s.handleContentUsageStats).Description = "Read content provider usage statistics"
|
||||
c.Action("content.usage_stats", s.handleContentUsageStats).Description = "Read content provider usage statistics"
|
||||
c.Action("content.from.plan", s.handleContentFromPlan).Description = "Generate content from plan context"
|
||||
c.Action("content.from_plan", s.handleContentFromPlan).Description = "Generate content from plan context"
|
||||
|
||||
c.Action("agentic.prompt", s.handlePrompt).Description = "Read a system prompt by slug"
|
||||
c.Action("agentic.task", s.handleTask).Description = "Read a task plan by slug"
|
||||
|
|
@ -326,6 +340,7 @@ func (s *PrepSubsystem) RegisterTools(server *mcp.Server) {
|
|||
s.registerTemplateTools(server)
|
||||
s.registerIssueTools(server)
|
||||
s.registerSprintTools(server)
|
||||
s.registerContentTools(server)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "agentic_scan",
|
||||
|
|
|
|||
|
|
@ -490,6 +490,28 @@ func TestPrep_OnStartup_Good_RegistersSessionActions(t *testing.T) {
|
|||
assert.True(t, c.Action("sprint.archive").Exists())
|
||||
}
|
||||
|
||||
func TestPrep_OnStartup_Good_RegistersContentActions(t *testing.T) {
|
||||
t.Setenv("CORE_WORKSPACE", t.TempDir())
|
||||
t.Setenv("CORE_AGENT_DISPATCH", "")
|
||||
|
||||
c := core.New(core.WithOption("name", "test"))
|
||||
s := NewPrep()
|
||||
s.ServiceRuntime = core.NewServiceRuntime(c, AgentOptions{})
|
||||
|
||||
require.True(t, s.OnStartup(context.Background()).OK)
|
||||
assert.True(t, c.Action("content.generate").Exists())
|
||||
assert.True(t, c.Action("content.batch.generate").Exists())
|
||||
assert.True(t, c.Action("content.batch_generate").Exists())
|
||||
assert.True(t, c.Action("content.brief.create").Exists())
|
||||
assert.True(t, c.Action("content.brief.get").Exists())
|
||||
assert.True(t, c.Action("content.brief.list").Exists())
|
||||
assert.True(t, c.Action("content.status").Exists())
|
||||
assert.True(t, c.Action("content.usage.stats").Exists())
|
||||
assert.True(t, c.Action("content.usage_stats").Exists())
|
||||
assert.True(t, c.Action("content.from.plan").Exists())
|
||||
assert.True(t, c.Action("content.from_plan").Exists())
|
||||
}
|
||||
|
||||
func TestPrep_OnStartup_Good_RegistersPlatformActionAliases(t *testing.T) {
|
||||
t.Setenv("CORE_WORKSPACE", t.TempDir())
|
||||
t.Setenv("CORE_AGENT_DISPATCH", "")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue