// 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{ // BriefID: "brief_1", // Template: "help-article", // Provider: "claude", // } type ContentGenerateInput struct { 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"` } // 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"` } // input := agentic.ContentSchemaInput{Type: "article", Title: "Release notes", URL: "https://example.test/releases"} type ContentSchemaInput struct { Type string `json:"type,omitempty"` Title string `json:"title,omitempty"` Description string `json:"description,omitempty"` URL string `json:"url,omitempty"` Author string `json:"author,omitempty"` PublishedAt string `json:"published_at,omitempty"` ModifiedAt string `json:"modified_at,omitempty"` Language string `json:"language,omitempty"` Image string `json:"image,omitempty"` Questions []ContentSchemaQuestion `json:"questions,omitempty"` Steps []ContentSchemaStep `json:"steps,omitempty"` } // question := agentic.ContentSchemaQuestion{Question: "What changed?", Answer: "The release notes are now generated from plans."} type ContentSchemaQuestion struct { Question string `json:"question"` Answer string `json:"answer"` } // step := agentic.ContentSchemaStep{Name: "Review", Text: "Check the draft for accuracy."} type ContentSchemaStep struct { Name string `json:"name,omitempty"` Text string `json:"text,omitempty"` URL string `json:"url,omitempty"` } // out := agentic.ContentSchemaOutput{Success: true, SchemaType: "FAQPage"} type ContentSchemaOutput struct { Success bool `json:"success"` SchemaType string `json:"schema_type"` SchemaJSON string `json:"schema_json"` Schema map[string]any `json:"schema"` } // 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"), BriefID: optionStringValue(options, "brief_id", "brief-id"), Template: optionStringValue(options, "template"), 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} } func (s *PrepSubsystem) contentGenerateResult(ctx context.Context, input ContentGenerateInput) (ContentResult, error) { if err := s.validateContentProvider(input.Provider); err != nil { return ContentResult{}, err } hasPrompt := core.Trim(input.Prompt) != "" hasBrief := core.Trim(input.BriefID) != "" hasTemplate := core.Trim(input.Template) != "" if !hasPrompt && !(hasBrief && hasTemplate) { return ContentResult{}, core.E("contentGenerate", "prompt or brief_id plus template is required", nil) } 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 } if len(input.Config) > 0 { body["config"] = input.Config } result := s.platformPayload(ctx, "content.generate", "POST", "/v1/content/generate", body) if !result.OK { return ContentResult{}, resultErrorValue("content.generate", result) } return parseContentResult(payloadResourceMap(result.Value.(map[string]any), "result", "content", "generation")), nil } func (s *PrepSubsystem) validateContentProvider(providerName string) error { if core.Trim(providerName) == "" { return nil } provider, ok := s.providerManager().Provider(providerName) if !ok { return core.E("contentGenerate", core.Concat("unknown provider: ", providerName), nil) } if !provider.IsAvailable() { return core.E("contentGenerate", core.Concat("provider unavailable: ", providerName), nil) } return nil } // 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} } // result := c.Action("content.schema.generate").Run(ctx, core.NewOptions( // // core.Option{Key: "type", Value: "howto"}, // core.Option{Key: "title", Value: "Set up the workspace"}, // // )) func (s *PrepSubsystem) handleContentSchemaGenerate(ctx context.Context, options core.Options) core.Result { input := ContentSchemaInput{ Type: optionStringValue(options, "schema_type", "schema-type", "type", "kind"), Title: optionStringValue(options, "title", "headline"), Description: optionStringValue(options, "description"), URL: optionStringValue(options, "url", "link"), Author: optionStringValue(options, "author"), PublishedAt: optionStringValue(options, "published_at", "published-at", "date_published"), ModifiedAt: optionStringValue(options, "modified_at", "modified-at", "date_modified"), Language: optionStringValue(options, "language", "in_language", "in-language"), Image: optionStringValue(options, "image", "image_url", "image-url"), } if value := optionAnyValue(options, "questions", "faq"); value != nil { input.Questions = contentSchemaQuestionsValue(value) } if value := optionAnyValue(options, "steps"); value != nil { input.Steps = contentSchemaStepsValue(value) } _, output, err := s.contentSchemaGenerate(ctx, nil, input) 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 from a prompt or a brief/template pair 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_batch", Description: "Generate content for a stored batch specification using the legacy MCP alias.", }, 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) mcp.AddTool(server, &mcp.Tool{ Name: "content_schema_generate", Description: "Generate SEO schema JSON-LD for article, FAQ, or how-to content.", }, s.contentSchemaGenerate) } func (s *PrepSubsystem) contentGenerate(ctx context.Context, _ *mcp.CallToolRequest, input ContentGenerateInput) (*mcp.CallToolResult, ContentGenerateOutput, error) { content, err := s.contentGenerateResult(ctx, input) if err != nil { return nil, ContentGenerateOutput{}, err } return nil, ContentGenerateOutput{ Success: true, Result: content, }, 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) } if err := s.validateContentProvider(input.Provider); err != nil { return nil, ContentBatchOutput{}, err } 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) } if err := s.validateContentProvider(input.Provider); err != nil { return nil, ContentFromPlanOutput{}, err } 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 (s *PrepSubsystem) contentSchemaGenerate(_ context.Context, _ *mcp.CallToolRequest, input ContentSchemaInput) (*mcp.CallToolResult, ContentSchemaOutput, error) { schemaType := contentSchemaType(input.Type) if schemaType == "" { return nil, ContentSchemaOutput{}, core.E("contentSchemaGenerate", "schema type is required", nil) } if core.Trim(input.Title) == "" { return nil, ContentSchemaOutput{}, core.E("contentSchemaGenerate", "title is required", nil) } schema := map[string]any{ "@context": "https://schema.org", "@type": schemaType, "name": input.Title, "headline": input.Title, } if input.Description != "" { schema["description"] = input.Description } if input.URL != "" { schema["url"] = input.URL schema["mainEntityOfPage"] = input.URL } if input.Author != "" { schema["author"] = map[string]any{ "@type": "Person", "name": input.Author, } } if input.PublishedAt != "" { schema["datePublished"] = input.PublishedAt } if input.ModifiedAt != "" { schema["dateModified"] = input.ModifiedAt } if input.Language != "" { schema["inLanguage"] = input.Language } if input.Image != "" { schema["image"] = input.Image } switch schemaType { case "FAQPage": if len(input.Questions) == 0 { return nil, ContentSchemaOutput{}, core.E("contentSchemaGenerate", "questions are required for FAQ schema", nil) } schema["mainEntity"] = contentSchemaFAQEntries(input.Questions) case "HowTo": if len(input.Steps) == 0 { return nil, ContentSchemaOutput{}, core.E("contentSchemaGenerate", "steps are required for how-to schema", nil) } schema["step"] = contentSchemaHowToSteps(input.Steps) case "BlogPosting", "TechArticle": if len(input.Steps) > 0 { schema["step"] = contentSchemaHowToSteps(input.Steps) } if len(input.Questions) > 0 { schema["mainEntity"] = contentSchemaFAQEntries(input.Questions) } default: if len(input.Questions) > 0 { schema["mainEntity"] = contentSchemaFAQEntries(input.Questions) } if len(input.Steps) > 0 { schema["step"] = contentSchemaHowToSteps(input.Steps) } } return nil, ContentSchemaOutput{ Success: true, SchemaType: schemaType, SchemaJSON: core.JSONMarshalString(schema), Schema: schema, }, 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 "" } func contentSchemaType(value string) string { switch core.Lower(core.Trim(value)) { case "article", "blog", "blogpost", "blog-post", "blogposting": return "BlogPosting" case "tech", "techarticle", "tech-article": return "TechArticle" case "faq", "faqpage": return "FAQPage" case "howto", "how-to": return "HowTo" } return "" } func contentSchemaQuestionsValue(value any) []ContentSchemaQuestion { items := contentSchemaItemsValue(value) if len(items) == 0 { return nil } questions := make([]ContentSchemaQuestion, 0, len(items)) for _, item := range items { question := contentMapStringValue(item, "question", "name", "title") answer := contentMapStringValue(item, "answer", "text", "body", "content") if question == "" || answer == "" { continue } questions = append(questions, ContentSchemaQuestion{ Question: question, Answer: answer, }) } return questions } func contentSchemaStepsValue(value any) []ContentSchemaStep { items := contentSchemaItemsValue(value) if len(items) == 0 { return nil } steps := make([]ContentSchemaStep, 0, len(items)) for _, item := range items { text := contentMapStringValue(item, "text", "body", "content", "description") name := contentMapStringValue(item, "name", "title", "label") url := contentMapStringValue(item, "url", "link") if name == "" && text == "" { continue } steps = append(steps, ContentSchemaStep{ Name: name, Text: text, URL: url, }) } return steps } func contentSchemaItemsValue(value any) []map[string]any { switch typed := value.(type) { case []ContentSchemaQuestion: items := make([]map[string]any, 0, len(typed)) for _, item := range typed { items = append(items, map[string]any{ "question": item.Question, "answer": item.Answer, }) } return items case []ContentSchemaStep: items := make([]map[string]any, 0, len(typed)) for _, item := range typed { items = append(items, map[string]any{ "name": item.Name, "text": item.Text, "url": item.URL, }) } return items case []map[string]any: return typed case []any: items := make([]map[string]any, 0, len(typed)) for _, item := range typed { if parsed := contentSchemaItemMap(item); len(parsed) > 0 { items = append(items, parsed) } } return items case map[string]any: return []map[string]any{typed} case string: trimmed := core.Trim(typed) if trimmed == "" { return nil } var generic []any if result := core.JSONUnmarshalString(trimmed, &generic); result.OK { return contentSchemaItemsValue(generic) } var single map[string]any if result := core.JSONUnmarshalString(trimmed, &single); result.OK { return []map[string]any{single} } } return nil } func contentSchemaItemMap(value any) map[string]any { switch typed := value.(type) { case map[string]any: return typed case ContentSchemaQuestion: return map[string]any{ "question": typed.Question, "answer": typed.Answer, } case ContentSchemaStep: return map[string]any{ "name": typed.Name, "text": typed.Text, "url": typed.URL, } case string: trimmed := core.Trim(typed) if trimmed == "" { return nil } var generic map[string]any if result := core.JSONUnmarshalString(trimmed, &generic); result.OK { return generic } } return nil } func contentSchemaFAQEntries(questions []ContentSchemaQuestion) []map[string]any { entries := make([]map[string]any, 0, len(questions)) for _, question := range questions { if core.Trim(question.Question) == "" || core.Trim(question.Answer) == "" { continue } entries = append(entries, map[string]any{ "@type": "Question", "name": question.Question, "acceptedAnswer": map[string]any{ "@type": "Answer", "text": question.Answer, }, }) } return entries } func contentSchemaHowToSteps(steps []ContentSchemaStep) []map[string]any { entries := make([]map[string]any, 0, len(steps)) for _, step := range steps { if core.Trim(step.Name) == "" && core.Trim(step.Text) == "" { continue } entry := map[string]any{ "@type": "HowToStep", } if step.Name != "" { entry["name"] = step.Name } if step.Text != "" { entry["text"] = step.Text } if step.URL != "" { entry["url"] = step.URL } entries = append(entries, entry) } return entries }