feat(learnings): add store and MCP tool layer #34
@@ -203,6 +203,7 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st
|
||||
Archive: tools.NewArchiveTool(db),
|
||||
Projects: tools.NewProjectsTool(db, activeProjects),
|
||||
Version: tools.NewVersionTool(cfg.MCP.ServerName, info),
|
||||
Learnings: tools.NewLearningsTool(db, activeProjects, cfg.Search),
|
||||
Context: tools.NewContextTool(db, embeddings, cfg.Search, activeProjects),
|
||||
Recall: tools.NewRecallTool(db, embeddings, cfg.Search, activeProjects),
|
||||
Summarize: tools.NewSummarizeTool(db, embeddings, metadata, cfg.Search, activeProjects),
|
||||
|
||||
@@ -40,6 +40,7 @@ type ToolSet struct {
|
||||
Skills *tools.SkillsTool
|
||||
ChatHistory *tools.ChatHistoryTool
|
||||
Describe *tools.DescribeTool
|
||||
Learnings *tools.LearningsTool
|
||||
}
|
||||
|
||||
// Handlers groups the HTTP handlers produced for an MCP server instance.
|
||||
@@ -83,6 +84,7 @@ func NewHandlers(cfg config.MCPConfig, logger *slog.Logger, toolSet ToolSet, onS
|
||||
registerSystemTools,
|
||||
registerThoughtTools,
|
||||
registerProjectTools,
|
||||
registerLearningTools,
|
||||
registerFileTools,
|
||||
registerMaintenanceTools,
|
||||
registerSkillTools,
|
||||
@@ -249,6 +251,28 @@ func registerProjectTools(server *mcp.Server, logger *slog.Logger, toolSet ToolS
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerLearningTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
|
||||
if err := addTool(server, logger, &mcp.Tool{
|
||||
Name: "add_learning",
|
||||
Description: "Create a curated learning record distinct from raw thoughts.",
|
||||
}, toolSet.Learnings.Add); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := addTool(server, logger, &mcp.Tool{
|
||||
Name: "get_learning",
|
||||
Description: "Retrieve a structured learning by id.",
|
||||
}, toolSet.Learnings.Get); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := addTool(server, logger, &mcp.Tool{
|
||||
Name: "list_learnings",
|
||||
Description: "List structured learnings with optional project, status, priority, tag, and text filters.",
|
||||
}, toolSet.Learnings.List); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerFileTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
|
||||
server.AddResourceTemplate(&mcp.ResourceTemplate{
|
||||
Name: "stored_file",
|
||||
@@ -477,6 +501,11 @@ func BuildToolCatalog() []tools.ToolEntry {
|
||||
{Name: "get_active_project", Description: "Return the active project for the current MCP session. If your client does not preserve MCP sessions, pass project explicitly to project-scoped tools instead of relying on this.", Category: "projects"},
|
||||
{Name: "get_project_context", Description: "Get recent and semantic context for a project. Uses the explicit project when provided, otherwise the active MCP session project. Falls back to full-text search when no embeddings exist.", Category: "projects"},
|
||||
|
||||
// learnings
|
||||
{Name: "add_learning", Description: "Create a curated learning record distinct from raw thoughts.", Category: "projects"},
|
||||
{Name: "get_learning", Description: "Retrieve a structured learning by id.", Category: "projects"},
|
||||
{Name: "list_learnings", Description: "List structured learnings with optional project, category, area, status, priority, tag, and text filters.", Category: "projects"},
|
||||
|
||||
// files
|
||||
{Name: "upload_file", Description: "Stage a file and get an amcs://files/{id} resource URI. Use content_path (absolute server-side path, no size limit) for large or binary files, or content_base64 (≤10 MB) for small files. Pass thought_id/project to link immediately, or omit and pass the URI to save_file later.", Category: "files"},
|
||||
{Name: "save_file", Description: "Store a file and optionally link it to a thought. Use content_base64 (≤10 MB) for small files, or content_uri (amcs://files/{id} from a prior upload_file) for previously staged files. For files larger than 10 MB, use upload_file with content_path first. If the goal is to retain the artifact, store the file directly instead of reading or summarising it first.", Category: "files"},
|
||||
|
||||
@@ -29,6 +29,7 @@ func TestNewListsAllRegisteredTools(t *testing.T) {
|
||||
|
||||
want := []string{
|
||||
"add_guardrail",
|
||||
"add_learning",
|
||||
"add_maintenance_task",
|
||||
"add_project_guardrail",
|
||||
"add_project_skill",
|
||||
@@ -43,6 +44,7 @@ func TestNewListsAllRegisteredTools(t *testing.T) {
|
||||
"describe_tools",
|
||||
"get_active_project",
|
||||
"get_chat_history",
|
||||
"get_learning",
|
||||
"get_project_context",
|
||||
"get_thought",
|
||||
"get_upcoming_maintenance",
|
||||
@@ -51,6 +53,7 @@ func TestNewListsAllRegisteredTools(t *testing.T) {
|
||||
"list_chat_histories",
|
||||
"list_files",
|
||||
"list_guardrails",
|
||||
"list_learnings",
|
||||
"list_project_guardrails",
|
||||
"list_project_skills",
|
||||
"list_projects",
|
||||
|
||||
215
internal/store/learnings.go
Normal file
215
internal/store/learnings.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
||||
)
|
||||
|
||||
func (db *DB) CreateLearning(ctx context.Context, learning thoughttypes.Learning) (thoughttypes.Learning, error) {
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
insert into learnings (
|
||||
summary, details, category, area, status, priority, confidence,
|
||||
action_required, source_type, source_ref, project_id, related_thought_id,
|
||||
related_skill_id, reviewed_by, reviewed_at, duplicate_of_learning_id,
|
||||
supersedes_learning_id, tags
|
||||
) values (
|
||||
$1, $2, $3, $4, $5, $6, $7,
|
||||
$8, $9, $10, $11, $12,
|
||||
$13, $14, $15, $16,
|
||||
$17, $18
|
||||
)
|
||||
returning id, created_at, updated_at
|
||||
`,
|
||||
strings.TrimSpace(learning.Summary),
|
||||
strings.TrimSpace(learning.Details),
|
||||
strings.TrimSpace(learning.Category),
|
||||
strings.TrimSpace(learning.Area),
|
||||
string(learning.Status),
|
||||
string(learning.Priority),
|
||||
string(learning.Confidence),
|
||||
learning.ActionRequired,
|
||||
nullableText(learning.SourceType),
|
||||
nullableText(learning.SourceRef),
|
||||
learning.ProjectID,
|
||||
learning.RelatedThoughtID,
|
||||
learning.RelatedSkillID,
|
||||
nullableTextPtr(learning.ReviewedBy),
|
||||
learning.ReviewedAt,
|
||||
learning.DuplicateOfLearningID,
|
||||
learning.SupersedesLearningID,
|
||||
learning.Tags,
|
||||
)
|
||||
|
||||
created := learning
|
||||
if err := row.Scan(&created.ID, &created.CreatedAt, &created.UpdatedAt); err != nil {
|
||||
return thoughttypes.Learning{}, fmt.Errorf("create learning: %w", err)
|
||||
}
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetLearning(ctx context.Context, id uuid.UUID) (thoughttypes.Learning, error) {
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
select id, summary, details, category, area, status, priority, confidence,
|
||||
action_required, source_type, source_ref, project_id, related_thought_id,
|
||||
related_skill_id, reviewed_by, reviewed_at, duplicate_of_learning_id,
|
||||
supersedes_learning_id, tags, created_at, updated_at
|
||||
from learnings
|
||||
where id = $1
|
||||
`, id)
|
||||
|
||||
learning, err := scanLearning(row)
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
return thoughttypes.Learning{}, fmt.Errorf("learning not found: %s", id)
|
||||
}
|
||||
return thoughttypes.Learning{}, fmt.Errorf("get learning: %w", err)
|
||||
}
|
||||
return learning, nil
|
||||
}
|
||||
|
||||
func (db *DB) ListLearnings(ctx context.Context, filter thoughttypes.LearningFilter) ([]thoughttypes.Learning, error) {
|
||||
args := make([]any, 0, 8)
|
||||
conditions := make([]string, 0, 8)
|
||||
|
||||
if filter.ProjectID != nil {
|
||||
args = append(args, *filter.ProjectID)
|
||||
conditions = append(conditions, fmt.Sprintf("project_id = $%d", len(args)))
|
||||
}
|
||||
if value := strings.TrimSpace(filter.Category); value != "" {
|
||||
args = append(args, value)
|
||||
conditions = append(conditions, fmt.Sprintf("category = $%d", len(args)))
|
||||
}
|
||||
if value := strings.TrimSpace(filter.Area); value != "" {
|
||||
args = append(args, value)
|
||||
conditions = append(conditions, fmt.Sprintf("area = $%d", len(args)))
|
||||
}
|
||||
if value := strings.TrimSpace(filter.Status); value != "" {
|
||||
args = append(args, value)
|
||||
conditions = append(conditions, fmt.Sprintf("status = $%d", len(args)))
|
||||
}
|
||||
if value := strings.TrimSpace(filter.Priority); value != "" {
|
||||
args = append(args, value)
|
||||
conditions = append(conditions, fmt.Sprintf("priority = $%d", len(args)))
|
||||
}
|
||||
if value := strings.TrimSpace(filter.Tag); value != "" {
|
||||
args = append(args, value)
|
||||
conditions = append(conditions, fmt.Sprintf("$%d = any(tags)", len(args)))
|
||||
}
|
||||
if value := strings.TrimSpace(filter.Query); value != "" {
|
||||
args = append(args, value)
|
||||
conditions = append(conditions, fmt.Sprintf("to_tsvector('simple', summary || ' ' || coalesce(details, '')) @@ websearch_to_tsquery('simple', $%d)", len(args)))
|
||||
}
|
||||
|
||||
query := `
|
||||
select id, summary, details, category, area, status, priority, confidence,
|
||||
action_required, source_type, source_ref, project_id, related_thought_id,
|
||||
related_skill_id, reviewed_by, reviewed_at, duplicate_of_learning_id,
|
||||
supersedes_learning_id, tags, created_at, updated_at
|
||||
from learnings
|
||||
`
|
||||
if len(conditions) > 0 {
|
||||
query += " where " + strings.Join(conditions, " and ")
|
||||
}
|
||||
query += " order by updated_at desc"
|
||||
if filter.Limit > 0 {
|
||||
args = append(args, filter.Limit)
|
||||
query += fmt.Sprintf(" limit $%d", len(args))
|
||||
}
|
||||
|
||||
rows, err := db.pool.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list learnings: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]thoughttypes.Learning, 0)
|
||||
for rows.Next() {
|
||||
item, err := scanLearning(rows)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan learning: %w", err)
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate learnings: %w", err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
type learningScanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
func scanLearning(row learningScanner) (thoughttypes.Learning, error) {
|
||||
var learning thoughttypes.Learning
|
||||
var sourceType pgtype.Text
|
||||
var sourceRef pgtype.Text
|
||||
var reviewedBy pgtype.Text
|
||||
var tags []string
|
||||
|
||||
err := row.Scan(
|
||||
&learning.ID,
|
||||
&learning.Summary,
|
||||
&learning.Details,
|
||||
&learning.Category,
|
||||
&learning.Area,
|
||||
&learning.Status,
|
||||
&learning.Priority,
|
||||
&learning.Confidence,
|
||||
&learning.ActionRequired,
|
||||
&sourceType,
|
||||
&sourceRef,
|
||||
&learning.ProjectID,
|
||||
&learning.RelatedThoughtID,
|
||||
&learning.RelatedSkillID,
|
||||
&reviewedBy,
|
||||
&learning.ReviewedAt,
|
||||
&learning.DuplicateOfLearningID,
|
||||
&learning.SupersedesLearningID,
|
||||
&tags,
|
||||
&learning.CreatedAt,
|
||||
&learning.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return thoughttypes.Learning{}, err
|
||||
}
|
||||
|
||||
learning.SourceType = sourceType.String
|
||||
learning.SourceRef = sourceRef.String
|
||||
if reviewedBy.Valid {
|
||||
value := reviewedBy.String
|
||||
learning.ReviewedBy = &value
|
||||
}
|
||||
if tags == nil {
|
||||
learning.Tags = []string{}
|
||||
} else {
|
||||
learning.Tags = tags
|
||||
}
|
||||
return learning, nil
|
||||
}
|
||||
|
||||
func nullableText(value string) *string {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return nil
|
||||
}
|
||||
return &trimmed
|
||||
}
|
||||
|
||||
func nullableTextPtr(value *string) *string {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
trimmed := strings.TrimSpace(*value)
|
||||
if trimmed == "" {
|
||||
return nil
|
||||
}
|
||||
return &trimmed
|
||||
}
|
||||
174
internal/tools/learnings.go
Normal file
174
internal/tools/learnings.go
Normal file
@@ -0,0 +1,174 @@
|
||||
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")
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
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 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
|
||||
}
|
||||
68
internal/types/learning.go
Normal file
68
internal/types/learning.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type LearningEvidenceLevel string
|
||||
|
||||
const (
|
||||
LearningEvidenceHypothesis LearningEvidenceLevel = "hypothesis"
|
||||
LearningEvidenceObserved LearningEvidenceLevel = "observed"
|
||||
LearningEvidenceVerified LearningEvidenceLevel = "verified"
|
||||
)
|
||||
|
||||
type LearningStatus string
|
||||
|
||||
const (
|
||||
LearningStatusPending LearningStatus = "pending"
|
||||
LearningStatusInProgress LearningStatus = "in_progress"
|
||||
LearningStatusResolved LearningStatus = "resolved"
|
||||
LearningStatusWontFix LearningStatus = "wont_fix"
|
||||
LearningStatusPromoted LearningStatus = "promoted"
|
||||
)
|
||||
|
||||
type LearningPriority string
|
||||
|
||||
const (
|
||||
LearningPriorityLow LearningPriority = "low"
|
||||
LearningPriorityMedium LearningPriority = "medium"
|
||||
LearningPriorityHigh LearningPriority = "high"
|
||||
)
|
||||
|
||||
type Learning struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Summary string `json:"summary"`
|
||||
Details string `json:"details"`
|
||||
Category string `json:"category"`
|
||||
Area string `json:"area"`
|
||||
Status LearningStatus `json:"status"`
|
||||
Priority LearningPriority `json:"priority"`
|
||||
Confidence LearningEvidenceLevel `json:"confidence"`
|
||||
ActionRequired bool `json:"action_required"`
|
||||
SourceType string `json:"source_type,omitempty"`
|
||||
SourceRef string `json:"source_ref,omitempty"`
|
||||
ProjectID *uuid.UUID `json:"project_id,omitempty"`
|
||||
RelatedThoughtID *uuid.UUID `json:"related_thought_id,omitempty"`
|
||||
RelatedSkillID *uuid.UUID `json:"related_skill_id,omitempty"`
|
||||
ReviewedBy *string `json:"reviewed_by,omitempty"`
|
||||
ReviewedAt *time.Time `json:"reviewed_at,omitempty"`
|
||||
DuplicateOfLearningID *uuid.UUID `json:"duplicate_of_learning_id,omitempty"`
|
||||
SupersedesLearningID *uuid.UUID `json:"supersedes_learning_id,omitempty"`
|
||||
Tags []string `json:"tags"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type LearningFilter struct {
|
||||
Limit int
|
||||
ProjectID *uuid.UUID
|
||||
Category string
|
||||
Area string
|
||||
Status string
|
||||
Priority string
|
||||
Tag string
|
||||
Query string
|
||||
}
|
||||
Reference in New Issue
Block a user