Some checks failed
CI / build-and-test (push) Failing after -31m53s
* Implement maintenance page with task and log display * Add backfill and metadata retry functionality * Integrate grid component for project display in thoughts page * Update types for maintenance tasks and logs * Enhance sidebar and shell for new maintenance navigation
193 lines
6.5 KiB
Go
193 lines
6.5 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
|
|
"git.warky.dev/wdevs/amcs/internal/config"
|
|
"git.warky.dev/wdevs/amcs/internal/session"
|
|
"git.warky.dev/wdevs/amcs/internal/store"
|
|
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
|
)
|
|
|
|
type LearningsTool struct {
|
|
store *store.DB
|
|
sessions *session.ActiveProjects
|
|
cfg config.SearchConfig
|
|
}
|
|
|
|
type AddLearningInput struct {
|
|
Summary string `json:"summary" jsonschema:"short curated learning summary"`
|
|
Details string `json:"details,omitempty" jsonschema:"optional detailed learning body"`
|
|
Category string `json:"category,omitempty"`
|
|
Area string `json:"area,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Priority string `json:"priority,omitempty"`
|
|
Confidence string `json:"confidence,omitempty"`
|
|
ActionRequired *bool `json:"action_required,omitempty"`
|
|
SourceType string `json:"source_type,omitempty"`
|
|
SourceRef string `json:"source_ref,omitempty"`
|
|
Project string `json:"project,omitempty" jsonschema:"project name or id; falls back to active session project"`
|
|
RelatedThoughtID *uuid.UUID `json:"related_thought_id,omitempty"`
|
|
RelatedSkillID *uuid.UUID `json:"related_skill_id,omitempty"`
|
|
ReviewedBy *string `json:"reviewed_by,omitempty"`
|
|
DuplicateOfLearningID *uuid.UUID `json:"duplicate_of_learning_id,omitempty"`
|
|
SupersedesLearningID *uuid.UUID `json:"supersedes_learning_id,omitempty"`
|
|
Tags []string `json:"tags,omitempty"`
|
|
}
|
|
|
|
type AddLearningOutput struct {
|
|
Learning thoughttypes.Learning `json:"learning"`
|
|
}
|
|
|
|
type GetLearningInput struct {
|
|
ID uuid.UUID `json:"id" jsonschema:"learning id"`
|
|
}
|
|
|
|
type GetLearningOutput struct {
|
|
Learning thoughttypes.Learning `json:"learning"`
|
|
}
|
|
|
|
type ListLearningsInput struct {
|
|
Limit int `json:"limit,omitempty"`
|
|
Project string `json:"project,omitempty" jsonschema:"project name or id; falls back to active session project"`
|
|
Category string `json:"category,omitempty"`
|
|
Area string `json:"area,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Priority string `json:"priority,omitempty"`
|
|
Tag string `json:"tag,omitempty"`
|
|
Query string `json:"query,omitempty"`
|
|
}
|
|
|
|
type ListLearningsOutput struct {
|
|
Learnings []thoughttypes.Learning `json:"learnings"`
|
|
}
|
|
|
|
func NewLearningsTool(db *store.DB, sessions *session.ActiveProjects, cfg config.SearchConfig) *LearningsTool {
|
|
return &LearningsTool{store: db, sessions: sessions, cfg: cfg}
|
|
}
|
|
|
|
func (t *LearningsTool) Add(ctx context.Context, req *mcp.CallToolRequest, in AddLearningInput) (*mcp.CallToolResult, AddLearningOutput, error) {
|
|
summary := strings.TrimSpace(in.Summary)
|
|
if summary == "" {
|
|
return nil, AddLearningOutput{}, errRequiredField("summary")
|
|
}
|
|
if err := t.ensureConfigured(); err != nil {
|
|
return nil, AddLearningOutput{}, err
|
|
}
|
|
|
|
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false)
|
|
if err != nil {
|
|
return nil, AddLearningOutput{}, err
|
|
}
|
|
|
|
learning := thoughttypes.Learning{
|
|
Summary: summary,
|
|
Details: strings.TrimSpace(in.Details),
|
|
Category: defaultString(strings.TrimSpace(in.Category), "insight"),
|
|
Area: defaultString(strings.TrimSpace(in.Area), "other"),
|
|
Status: thoughttypes.LearningStatus(defaultString(strings.TrimSpace(in.Status), string(thoughttypes.LearningStatusPending))),
|
|
Priority: thoughttypes.LearningPriority(defaultString(strings.TrimSpace(in.Priority), string(thoughttypes.LearningPriorityMedium))),
|
|
Confidence: thoughttypes.LearningEvidenceLevel(defaultString(strings.TrimSpace(in.Confidence), string(thoughttypes.LearningEvidenceHypothesis))),
|
|
SourceType: strings.TrimSpace(in.SourceType),
|
|
SourceRef: strings.TrimSpace(in.SourceRef),
|
|
RelatedThoughtID: in.RelatedThoughtID,
|
|
RelatedSkillID: in.RelatedSkillID,
|
|
ReviewedBy: in.ReviewedBy,
|
|
DuplicateOfLearningID: in.DuplicateOfLearningID,
|
|
SupersedesLearningID: in.SupersedesLearningID,
|
|
Tags: normalizeStringSlice(in.Tags),
|
|
}
|
|
if in.ActionRequired != nil {
|
|
learning.ActionRequired = *in.ActionRequired
|
|
}
|
|
if project != nil {
|
|
learning.ProjectID = &project.ID
|
|
}
|
|
|
|
created, err := t.store.CreateLearning(ctx, learning)
|
|
if err != nil {
|
|
return nil, AddLearningOutput{}, err
|
|
}
|
|
return nil, AddLearningOutput{Learning: created}, nil
|
|
}
|
|
|
|
func (t *LearningsTool) Get(ctx context.Context, _ *mcp.CallToolRequest, in GetLearningInput) (*mcp.CallToolResult, GetLearningOutput, error) {
|
|
if err := t.ensureConfigured(); err != nil {
|
|
return nil, GetLearningOutput{}, err
|
|
}
|
|
|
|
learning, err := t.store.GetLearning(ctx, in.ID)
|
|
if err != nil {
|
|
return nil, GetLearningOutput{}, err
|
|
}
|
|
return nil, GetLearningOutput{Learning: learning}, nil
|
|
}
|
|
|
|
func (t *LearningsTool) List(ctx context.Context, req *mcp.CallToolRequest, in ListLearningsInput) (*mcp.CallToolResult, ListLearningsOutput, error) {
|
|
if err := t.ensureConfigured(); err != nil {
|
|
return nil, ListLearningsOutput{}, err
|
|
}
|
|
|
|
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false)
|
|
if err != nil {
|
|
return nil, ListLearningsOutput{}, err
|
|
}
|
|
|
|
filter := thoughttypes.LearningFilter{
|
|
Limit: normalizeLimit(in.Limit, t.cfg),
|
|
Category: strings.TrimSpace(in.Category),
|
|
Area: strings.TrimSpace(in.Area),
|
|
Status: strings.TrimSpace(in.Status),
|
|
Priority: strings.TrimSpace(in.Priority),
|
|
Tag: strings.TrimSpace(in.Tag),
|
|
Query: strings.TrimSpace(in.Query),
|
|
}
|
|
if project != nil {
|
|
filter.ProjectID = &project.ID
|
|
}
|
|
|
|
items, err := t.store.ListLearnings(ctx, filter)
|
|
if err != nil {
|
|
return nil, ListLearningsOutput{}, err
|
|
}
|
|
return nil, ListLearningsOutput{Learnings: items}, nil
|
|
}
|
|
|
|
func (t *LearningsTool) ensureConfigured() error {
|
|
if t == nil || t.store == nil {
|
|
return errInvalidInput("learnings tool is not configured")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func defaultString(value string, fallback string) string {
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
return value
|
|
}
|
|
|
|
func normalizeStringSlice(values []string) []string {
|
|
if len(values) == 0 {
|
|
return []string{}
|
|
}
|
|
out := make([]string, 0, len(values))
|
|
seen := map[string]struct{}{}
|
|
for _, value := range values {
|
|
trimmed := strings.TrimSpace(value)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[trimmed]; ok {
|
|
continue
|
|
}
|
|
seen[trimmed] = struct{}{}
|
|
out = append(out, trimmed)
|
|
}
|
|
return out
|
|
}
|