feat(tools): implement CRUD operations for thoughts and projects
* Add tools for creating, retrieving, updating, and deleting thoughts. * Implement project management tools for creating and listing projects. * Introduce linking functionality between thoughts. * Add search and recall capabilities for thoughts based on semantic queries. * Implement statistics and summarization tools for thought analysis. * Create database migrations for thoughts, projects, and links. * Add helper functions for UUID parsing and project resolution.
This commit is contained in:
36
internal/tools/archive.go
Normal file
36
internal/tools/archive.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/store"
|
||||
)
|
||||
|
||||
type ArchiveTool struct {
|
||||
store *store.DB
|
||||
}
|
||||
|
||||
type ArchiveInput struct {
|
||||
ID string `json:"id" jsonschema:"the thought id"`
|
||||
}
|
||||
|
||||
type ArchiveOutput struct {
|
||||
Archived bool `json:"archived"`
|
||||
}
|
||||
|
||||
func NewArchiveTool(db *store.DB) *ArchiveTool {
|
||||
return &ArchiveTool{store: db}
|
||||
}
|
||||
|
||||
func (t *ArchiveTool) Handle(ctx context.Context, _ *mcp.CallToolRequest, in ArchiveInput) (*mcp.CallToolResult, ArchiveOutput, error) {
|
||||
id, err := parseUUID(in.ID)
|
||||
if err != nil {
|
||||
return nil, ArchiveOutput{}, err
|
||||
}
|
||||
if err := t.store.ArchiveThought(ctx, id); err != nil {
|
||||
return nil, ArchiveOutput{}, err
|
||||
}
|
||||
return nil, ArchiveOutput{Archived: true}, nil
|
||||
}
|
||||
95
internal/tools/capture.go
Normal file
95
internal/tools/capture.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/ai"
|
||||
"git.warky.dev/wdevs/amcs/internal/config"
|
||||
"git.warky.dev/wdevs/amcs/internal/metadata"
|
||||
"git.warky.dev/wdevs/amcs/internal/session"
|
||||
"git.warky.dev/wdevs/amcs/internal/store"
|
||||
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
||||
)
|
||||
|
||||
type CaptureTool struct {
|
||||
store *store.DB
|
||||
provider ai.Provider
|
||||
capture config.CaptureConfig
|
||||
sessions *session.ActiveProjects
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
type CaptureInput struct {
|
||||
Content string `json:"content" jsonschema:"the thought or note to capture"`
|
||||
Project string `json:"project,omitempty" jsonschema:"optional project name or id to associate with the thought"`
|
||||
}
|
||||
|
||||
type CaptureOutput struct {
|
||||
Thought thoughttypes.Thought `json:"thought"`
|
||||
}
|
||||
|
||||
func NewCaptureTool(db *store.DB, provider ai.Provider, capture config.CaptureConfig, sessions *session.ActiveProjects, log *slog.Logger) *CaptureTool {
|
||||
return &CaptureTool{store: db, provider: provider, capture: capture, sessions: sessions, log: log}
|
||||
}
|
||||
|
||||
func (t *CaptureTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in CaptureInput) (*mcp.CallToolResult, CaptureOutput, error) {
|
||||
content := strings.TrimSpace(in.Content)
|
||||
if content == "" {
|
||||
return nil, CaptureOutput{}, errInvalidInput("content is required")
|
||||
}
|
||||
|
||||
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false)
|
||||
if err != nil {
|
||||
return nil, CaptureOutput{}, err
|
||||
}
|
||||
|
||||
var embedding []float32
|
||||
rawMetadata := metadata.Fallback(t.capture)
|
||||
|
||||
group, groupCtx := errgroup.WithContext(ctx)
|
||||
group.Go(func() error {
|
||||
vector, err := t.provider.Embed(groupCtx, content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
embedding = vector
|
||||
return nil
|
||||
})
|
||||
group.Go(func() error {
|
||||
extracted, err := t.provider.ExtractMetadata(groupCtx, content)
|
||||
if err != nil {
|
||||
t.log.Warn("metadata extraction failed, using fallback", slog.String("provider", t.provider.Name()), slog.String("error", err.Error()))
|
||||
return nil
|
||||
}
|
||||
rawMetadata = extracted
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := group.Wait(); err != nil {
|
||||
return nil, CaptureOutput{}, err
|
||||
}
|
||||
|
||||
thought := thoughttypes.Thought{
|
||||
Content: content,
|
||||
Embedding: embedding,
|
||||
Metadata: metadata.Normalize(rawMetadata, t.capture),
|
||||
}
|
||||
if project != nil {
|
||||
thought.ProjectID = &project.ID
|
||||
}
|
||||
|
||||
created, err := t.store.InsertThought(ctx, thought)
|
||||
if err != nil {
|
||||
return nil, CaptureOutput{}, err
|
||||
}
|
||||
if project != nil {
|
||||
_ = t.store.TouchProject(ctx, project.ID)
|
||||
}
|
||||
|
||||
return nil, CaptureOutput{Thought: created}, nil
|
||||
}
|
||||
31
internal/tools/common.go
Normal file
31
internal/tools/common.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/config"
|
||||
)
|
||||
|
||||
func normalizeLimit(limit int, cfg config.SearchConfig) int {
|
||||
if limit <= 0 {
|
||||
return cfg.DefaultLimit
|
||||
}
|
||||
if limit > cfg.MaxLimit {
|
||||
return cfg.MaxLimit
|
||||
}
|
||||
return limit
|
||||
}
|
||||
|
||||
func normalizeThreshold(value float64, fallback float64) float64 {
|
||||
if value <= 0 {
|
||||
return fallback
|
||||
}
|
||||
if value > 1 {
|
||||
return 1
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func errInvalidInput(message string) error {
|
||||
return fmt.Errorf("invalid input: %s", message)
|
||||
}
|
||||
111
internal/tools/context.go
Normal file
111
internal/tools/context.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/ai"
|
||||
"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 ContextTool struct {
|
||||
store *store.DB
|
||||
provider ai.Provider
|
||||
search config.SearchConfig
|
||||
sessions *session.ActiveProjects
|
||||
}
|
||||
|
||||
type ProjectContextInput struct {
|
||||
Project string `json:"project,omitempty" jsonschema:"project name or id; falls back to the active session project"`
|
||||
Query string `json:"query,omitempty" jsonschema:"optional semantic focus for project context"`
|
||||
Limit int `json:"limit,omitempty" jsonschema:"maximum number of context items to return"`
|
||||
}
|
||||
|
||||
type ContextItem struct {
|
||||
ID string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Metadata thoughttypes.ThoughtMetadata `json:"metadata"`
|
||||
Similarity float64 `json:"similarity,omitempty"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
type ProjectContextOutput struct {
|
||||
Project thoughttypes.Project `json:"project"`
|
||||
Context string `json:"context"`
|
||||
Items []ContextItem `json:"items"`
|
||||
}
|
||||
|
||||
func NewContextTool(db *store.DB, provider ai.Provider, search config.SearchConfig, sessions *session.ActiveProjects) *ContextTool {
|
||||
return &ContextTool{store: db, provider: provider, search: search, sessions: sessions}
|
||||
}
|
||||
|
||||
func (t *ContextTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in ProjectContextInput) (*mcp.CallToolResult, ProjectContextOutput, error) {
|
||||
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
|
||||
if err != nil {
|
||||
return nil, ProjectContextOutput{}, err
|
||||
}
|
||||
|
||||
limit := normalizeLimit(in.Limit, t.search)
|
||||
recent, err := t.store.RecentThoughts(ctx, &project.ID, limit, 0)
|
||||
if err != nil {
|
||||
return nil, ProjectContextOutput{}, err
|
||||
}
|
||||
|
||||
items := make([]ContextItem, 0, limit*2)
|
||||
seen := map[string]struct{}{}
|
||||
for _, thought := range recent {
|
||||
key := thought.ID.String()
|
||||
seen[key] = struct{}{}
|
||||
items = append(items, ContextItem{
|
||||
ID: key,
|
||||
Content: thought.Content,
|
||||
Metadata: thought.Metadata,
|
||||
Source: "recent",
|
||||
})
|
||||
}
|
||||
|
||||
query := strings.TrimSpace(in.Query)
|
||||
if query != "" {
|
||||
embedding, err := t.provider.Embed(ctx, query)
|
||||
if err != nil {
|
||||
return nil, ProjectContextOutput{}, err
|
||||
}
|
||||
semantic, err := t.store.SearchSimilarThoughts(ctx, embedding, t.search.DefaultThreshold, limit, &project.ID, nil)
|
||||
if err != nil {
|
||||
return nil, ProjectContextOutput{}, err
|
||||
}
|
||||
for _, result := range semantic {
|
||||
key := result.ID.String()
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
items = append(items, ContextItem{
|
||||
ID: key,
|
||||
Content: result.Content,
|
||||
Metadata: result.Metadata,
|
||||
Similarity: result.Similarity,
|
||||
Source: "semantic",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
lines := make([]string, 0, len(items))
|
||||
for i, item := range items {
|
||||
lines = append(lines, thoughtContextLine(i, item.Content, item.Metadata, item.Similarity))
|
||||
}
|
||||
contextBlock := formatContextBlock(fmt.Sprintf("Project context for %s", project.Name), lines)
|
||||
_ = t.store.TouchProject(ctx, project.ID)
|
||||
|
||||
return nil, ProjectContextOutput{
|
||||
Project: *project,
|
||||
Context: contextBlock,
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
36
internal/tools/delete.go
Normal file
36
internal/tools/delete.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/store"
|
||||
)
|
||||
|
||||
type DeleteTool struct {
|
||||
store *store.DB
|
||||
}
|
||||
|
||||
type DeleteInput struct {
|
||||
ID string `json:"id" jsonschema:"the thought id"`
|
||||
}
|
||||
|
||||
type DeleteOutput struct {
|
||||
Deleted bool `json:"deleted"`
|
||||
}
|
||||
|
||||
func NewDeleteTool(db *store.DB) *DeleteTool {
|
||||
return &DeleteTool{store: db}
|
||||
}
|
||||
|
||||
func (t *DeleteTool) Handle(ctx context.Context, _ *mcp.CallToolRequest, in DeleteInput) (*mcp.CallToolResult, DeleteOutput, error) {
|
||||
id, err := parseUUID(in.ID)
|
||||
if err != nil {
|
||||
return nil, DeleteOutput{}, err
|
||||
}
|
||||
if err := t.store.DeleteThought(ctx, id); err != nil {
|
||||
return nil, DeleteOutput{}, err
|
||||
}
|
||||
return nil, DeleteOutput{Deleted: true}, nil
|
||||
}
|
||||
40
internal/tools/get.go
Normal file
40
internal/tools/get.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/store"
|
||||
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
||||
)
|
||||
|
||||
type GetTool struct {
|
||||
store *store.DB
|
||||
}
|
||||
|
||||
type GetInput struct {
|
||||
ID string `json:"id" jsonschema:"the thought id"`
|
||||
}
|
||||
|
||||
type GetOutput struct {
|
||||
Thought thoughttypes.Thought `json:"thought"`
|
||||
}
|
||||
|
||||
func NewGetTool(db *store.DB) *GetTool {
|
||||
return &GetTool{store: db}
|
||||
}
|
||||
|
||||
func (t *GetTool) Handle(ctx context.Context, _ *mcp.CallToolRequest, in GetInput) (*mcp.CallToolResult, GetOutput, error) {
|
||||
id, err := parseUUID(in.ID)
|
||||
if err != nil {
|
||||
return nil, GetOutput{}, err
|
||||
}
|
||||
|
||||
thought, err := t.store.GetThought(ctx, id)
|
||||
if err != nil {
|
||||
return nil, GetOutput{}, err
|
||||
}
|
||||
|
||||
return nil, GetOutput{Thought: thought}, nil
|
||||
}
|
||||
87
internal/tools/helpers.go
Normal file
87
internal/tools/helpers.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/session"
|
||||
"git.warky.dev/wdevs/amcs/internal/store"
|
||||
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
||||
)
|
||||
|
||||
func parseUUID(id string) (uuid.UUID, error) {
|
||||
parsed, err := uuid.Parse(strings.TrimSpace(id))
|
||||
if err != nil {
|
||||
return uuid.Nil, fmt.Errorf("invalid id %q: %w", id, err)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func sessionID(req *mcp.CallToolRequest) (string, error) {
|
||||
if req == nil || req.Session == nil || req.Session.ID() == "" {
|
||||
return "", fmt.Errorf("tool requires an MCP session")
|
||||
}
|
||||
return req.Session.ID(), nil
|
||||
}
|
||||
|
||||
func resolveProject(ctx context.Context, db *store.DB, sessions *session.ActiveProjects, req *mcp.CallToolRequest, raw string, required bool) (*thoughttypes.Project, error) {
|
||||
projectRef := strings.TrimSpace(raw)
|
||||
if projectRef == "" && sessions != nil && req != nil && req.Session != nil {
|
||||
if activeID, ok := sessions.Get(req.Session.ID()); ok {
|
||||
project, err := db.GetProject(ctx, activeID.String())
|
||||
if err == nil {
|
||||
return &project, nil
|
||||
}
|
||||
if err != pgx.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if projectRef == "" {
|
||||
if required {
|
||||
return nil, fmt.Errorf("project is required")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
project, err := db.GetProject(ctx, projectRef)
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, fmt.Errorf("project %q not found", projectRef)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &project, nil
|
||||
}
|
||||
|
||||
func formatContextBlock(header string, lines []string) string {
|
||||
if len(lines) == 0 {
|
||||
return header + "\n\nNo matching thoughts."
|
||||
}
|
||||
return header + "\n\n" + strings.Join(lines, "\n\n")
|
||||
}
|
||||
|
||||
func thoughtContextLine(index int, content string, metadata thoughttypes.ThoughtMetadata, similarity float64) string {
|
||||
label := fmt.Sprintf("%d. %s", index+1, strings.TrimSpace(content))
|
||||
parts := make([]string, 0, 3)
|
||||
if len(metadata.Topics) > 0 {
|
||||
parts = append(parts, "topics="+strings.Join(metadata.Topics, ", "))
|
||||
}
|
||||
if metadata.Type != "" {
|
||||
parts = append(parts, "type="+metadata.Type)
|
||||
}
|
||||
if similarity > 0 {
|
||||
parts = append(parts, fmt.Sprintf("similarity=%.3f", similarity))
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return label
|
||||
}
|
||||
return label + "\n[" + strings.Join(parts, " | ") + "]"
|
||||
}
|
||||
145
internal/tools/links.go
Normal file
145
internal/tools/links.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/ai"
|
||||
"git.warky.dev/wdevs/amcs/internal/config"
|
||||
"git.warky.dev/wdevs/amcs/internal/store"
|
||||
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
||||
)
|
||||
|
||||
type LinksTool struct {
|
||||
store *store.DB
|
||||
provider ai.Provider
|
||||
search config.SearchConfig
|
||||
}
|
||||
|
||||
type LinkInput struct {
|
||||
FromID string `json:"from_id" jsonschema:"the source thought id"`
|
||||
ToID string `json:"to_id" jsonschema:"the target thought id"`
|
||||
Relation string `json:"relation" jsonschema:"relationship label such as follows_up or references"`
|
||||
}
|
||||
|
||||
type LinkOutput struct {
|
||||
Linked bool `json:"linked"`
|
||||
}
|
||||
|
||||
type RelatedInput struct {
|
||||
ID string `json:"id" jsonschema:"the thought id"`
|
||||
IncludeSemantic *bool `json:"include_semantic,omitempty" jsonschema:"whether to include semantic neighbors; defaults to true"`
|
||||
}
|
||||
|
||||
type RelatedThought struct {
|
||||
ID string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Metadata thoughttypes.ThoughtMetadata `json:"metadata"`
|
||||
Relation string `json:"relation,omitempty"`
|
||||
Direction string `json:"direction,omitempty"`
|
||||
Similarity float64 `json:"similarity,omitempty"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
type RelatedOutput struct {
|
||||
Related []RelatedThought `json:"related"`
|
||||
}
|
||||
|
||||
func NewLinksTool(db *store.DB, provider ai.Provider, search config.SearchConfig) *LinksTool {
|
||||
return &LinksTool{store: db, provider: provider, search: search}
|
||||
}
|
||||
|
||||
func (t *LinksTool) Link(ctx context.Context, _ *mcp.CallToolRequest, in LinkInput) (*mcp.CallToolResult, LinkOutput, error) {
|
||||
fromID, err := parseUUID(in.FromID)
|
||||
if err != nil {
|
||||
return nil, LinkOutput{}, err
|
||||
}
|
||||
toID, err := parseUUID(in.ToID)
|
||||
if err != nil {
|
||||
return nil, LinkOutput{}, err
|
||||
}
|
||||
relation := strings.TrimSpace(in.Relation)
|
||||
if relation == "" {
|
||||
return nil, LinkOutput{}, errInvalidInput("relation is required")
|
||||
}
|
||||
if _, err := t.store.GetThought(ctx, fromID); err != nil {
|
||||
return nil, LinkOutput{}, err
|
||||
}
|
||||
if _, err := t.store.GetThought(ctx, toID); err != nil {
|
||||
return nil, LinkOutput{}, err
|
||||
}
|
||||
if err := t.store.InsertLink(ctx, thoughttypes.ThoughtLink{
|
||||
FromID: fromID,
|
||||
ToID: toID,
|
||||
Relation: relation,
|
||||
}); err != nil {
|
||||
return nil, LinkOutput{}, err
|
||||
}
|
||||
return nil, LinkOutput{Linked: true}, nil
|
||||
}
|
||||
|
||||
func (t *LinksTool) Related(ctx context.Context, _ *mcp.CallToolRequest, in RelatedInput) (*mcp.CallToolResult, RelatedOutput, error) {
|
||||
id, err := parseUUID(in.ID)
|
||||
if err != nil {
|
||||
return nil, RelatedOutput{}, err
|
||||
}
|
||||
|
||||
thought, err := t.store.GetThought(ctx, id)
|
||||
if err != nil {
|
||||
return nil, RelatedOutput{}, err
|
||||
}
|
||||
|
||||
linked, err := t.store.LinkedThoughts(ctx, id)
|
||||
if err != nil {
|
||||
return nil, RelatedOutput{}, err
|
||||
}
|
||||
|
||||
related := make([]RelatedThought, 0, len(linked)+t.search.DefaultLimit)
|
||||
seen := map[string]struct{}{thought.ID.String(): {}}
|
||||
for _, item := range linked {
|
||||
key := item.Thought.ID.String()
|
||||
seen[key] = struct{}{}
|
||||
related = append(related, RelatedThought{
|
||||
ID: key,
|
||||
Content: item.Thought.Content,
|
||||
Metadata: item.Thought.Metadata,
|
||||
Relation: item.Relation,
|
||||
Direction: item.Direction,
|
||||
Source: "link",
|
||||
})
|
||||
}
|
||||
|
||||
includeSemantic := true
|
||||
if in.IncludeSemantic != nil {
|
||||
includeSemantic = *in.IncludeSemantic
|
||||
}
|
||||
|
||||
if includeSemantic {
|
||||
embedding, err := t.provider.Embed(ctx, thought.Content)
|
||||
if err != nil {
|
||||
return nil, RelatedOutput{}, err
|
||||
}
|
||||
semantic, err := t.store.SearchSimilarThoughts(ctx, embedding, t.search.DefaultThreshold, t.search.DefaultLimit, thought.ProjectID, &thought.ID)
|
||||
if err != nil {
|
||||
return nil, RelatedOutput{}, err
|
||||
}
|
||||
for _, item := range semantic {
|
||||
key := item.ID.String()
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
related = append(related, RelatedThought{
|
||||
ID: key,
|
||||
Content: item.Content,
|
||||
Metadata: item.Metadata,
|
||||
Similarity: item.Similarity,
|
||||
Source: "semantic",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil, RelatedOutput{Related: related}, nil
|
||||
}
|
||||
68
internal/tools/list.go
Normal file
68
internal/tools/list.go
Normal file
@@ -0,0 +1,68 @@
|
||||
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 ListTool struct {
|
||||
store *store.DB
|
||||
search config.SearchConfig
|
||||
sessions *session.ActiveProjects
|
||||
}
|
||||
|
||||
type ListInput struct {
|
||||
Limit int `json:"limit,omitempty" jsonschema:"maximum number of thoughts to return"`
|
||||
Type string `json:"type,omitempty" jsonschema:"filter by thought type"`
|
||||
Topic string `json:"topic,omitempty" jsonschema:"filter by topic"`
|
||||
Person string `json:"person,omitempty" jsonschema:"filter by mentioned person"`
|
||||
Days int `json:"days,omitempty" jsonschema:"only include thoughts from the last N days"`
|
||||
IncludeArchived bool `json:"include_archived,omitempty" jsonschema:"include archived thoughts when true"`
|
||||
Project string `json:"project,omitempty" jsonschema:"optional project name or id to scope the listing"`
|
||||
}
|
||||
|
||||
type ListOutput struct {
|
||||
Thoughts []thoughttypes.Thought `json:"thoughts"`
|
||||
}
|
||||
|
||||
func NewListTool(db *store.DB, search config.SearchConfig, sessions *session.ActiveProjects) *ListTool {
|
||||
return &ListTool{store: db, search: search, sessions: sessions}
|
||||
}
|
||||
|
||||
func (t *ListTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in ListInput) (*mcp.CallToolResult, ListOutput, error) {
|
||||
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false)
|
||||
if err != nil {
|
||||
return nil, ListOutput{}, err
|
||||
}
|
||||
|
||||
var projectID *uuid.UUID
|
||||
if project != nil {
|
||||
projectID = &project.ID
|
||||
}
|
||||
|
||||
thoughts, err := t.store.ListThoughts(ctx, thoughttypes.ListFilter{
|
||||
Limit: normalizeLimit(in.Limit, t.search),
|
||||
Type: strings.TrimSpace(in.Type),
|
||||
Topic: strings.TrimSpace(in.Topic),
|
||||
Person: strings.TrimSpace(in.Person),
|
||||
Days: in.Days,
|
||||
IncludeArchived: in.IncludeArchived,
|
||||
ProjectID: projectID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, ListOutput{}, err
|
||||
}
|
||||
if project != nil {
|
||||
_ = t.store.TouchProject(ctx, project.ID)
|
||||
}
|
||||
|
||||
return nil, ListOutput{Thoughts: thoughts}, nil
|
||||
}
|
||||
92
internal/tools/projects.go
Normal file
92
internal/tools/projects.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/session"
|
||||
"git.warky.dev/wdevs/amcs/internal/store"
|
||||
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
||||
)
|
||||
|
||||
type ProjectsTool struct {
|
||||
store *store.DB
|
||||
sessions *session.ActiveProjects
|
||||
}
|
||||
|
||||
type CreateProjectInput struct {
|
||||
Name string `json:"name" jsonschema:"the unique project name"`
|
||||
Description string `json:"description,omitempty" jsonschema:"optional project description"`
|
||||
}
|
||||
|
||||
type CreateProjectOutput struct {
|
||||
Project thoughttypes.Project `json:"project"`
|
||||
}
|
||||
|
||||
type ListProjectsInput struct{}
|
||||
|
||||
type ListProjectsOutput struct {
|
||||
Projects []thoughttypes.ProjectSummary `json:"projects"`
|
||||
}
|
||||
|
||||
type SetActiveProjectInput struct {
|
||||
Project string `json:"project" jsonschema:"project name or id"`
|
||||
}
|
||||
|
||||
type SetActiveProjectOutput struct {
|
||||
Project thoughttypes.Project `json:"project"`
|
||||
}
|
||||
|
||||
type GetActiveProjectInput struct{}
|
||||
|
||||
type GetActiveProjectOutput struct {
|
||||
Project *thoughttypes.Project `json:"project,omitempty"`
|
||||
}
|
||||
|
||||
func NewProjectsTool(db *store.DB, sessions *session.ActiveProjects) *ProjectsTool {
|
||||
return &ProjectsTool{store: db, sessions: sessions}
|
||||
}
|
||||
|
||||
func (t *ProjectsTool) Create(ctx context.Context, _ *mcp.CallToolRequest, in CreateProjectInput) (*mcp.CallToolResult, CreateProjectOutput, error) {
|
||||
name := strings.TrimSpace(in.Name)
|
||||
if name == "" {
|
||||
return nil, CreateProjectOutput{}, errInvalidInput("name is required")
|
||||
}
|
||||
project, err := t.store.CreateProject(ctx, name, strings.TrimSpace(in.Description))
|
||||
if err != nil {
|
||||
return nil, CreateProjectOutput{}, err
|
||||
}
|
||||
return nil, CreateProjectOutput{Project: project}, nil
|
||||
}
|
||||
|
||||
func (t *ProjectsTool) List(ctx context.Context, _ *mcp.CallToolRequest, _ ListProjectsInput) (*mcp.CallToolResult, ListProjectsOutput, error) {
|
||||
projects, err := t.store.ListProjects(ctx)
|
||||
if err != nil {
|
||||
return nil, ListProjectsOutput{}, err
|
||||
}
|
||||
return nil, ListProjectsOutput{Projects: projects}, nil
|
||||
}
|
||||
|
||||
func (t *ProjectsTool) SetActive(ctx context.Context, req *mcp.CallToolRequest, in SetActiveProjectInput) (*mcp.CallToolResult, SetActiveProjectOutput, error) {
|
||||
sid, err := sessionID(req)
|
||||
if err != nil {
|
||||
return nil, SetActiveProjectOutput{}, err
|
||||
}
|
||||
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
|
||||
if err != nil {
|
||||
return nil, SetActiveProjectOutput{}, err
|
||||
}
|
||||
t.sessions.Set(sid, project.ID)
|
||||
_ = t.store.TouchProject(ctx, project.ID)
|
||||
return nil, SetActiveProjectOutput{Project: *project}, nil
|
||||
}
|
||||
|
||||
func (t *ProjectsTool) GetActive(ctx context.Context, req *mcp.CallToolRequest, _ GetActiveProjectInput) (*mcp.CallToolResult, GetActiveProjectOutput, error) {
|
||||
project, err := resolveProject(ctx, t.store, t.sessions, req, "", false)
|
||||
if err != nil {
|
||||
return nil, GetActiveProjectOutput{}, err
|
||||
}
|
||||
return nil, GetActiveProjectOutput{Project: project}, nil
|
||||
}
|
||||
111
internal/tools/recall.go
Normal file
111
internal/tools/recall.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/ai"
|
||||
"git.warky.dev/wdevs/amcs/internal/config"
|
||||
"git.warky.dev/wdevs/amcs/internal/session"
|
||||
"git.warky.dev/wdevs/amcs/internal/store"
|
||||
)
|
||||
|
||||
type RecallTool struct {
|
||||
store *store.DB
|
||||
provider ai.Provider
|
||||
search config.SearchConfig
|
||||
sessions *session.ActiveProjects
|
||||
}
|
||||
|
||||
type RecallInput struct {
|
||||
Query string `json:"query" jsonschema:"semantic query for recalled context"`
|
||||
Project string `json:"project,omitempty" jsonschema:"optional project name or id; falls back to the active session project"`
|
||||
Limit int `json:"limit,omitempty" jsonschema:"maximum number of context items to return"`
|
||||
}
|
||||
|
||||
type RecallOutput struct {
|
||||
Context string `json:"context"`
|
||||
Items []ContextItem `json:"items"`
|
||||
}
|
||||
|
||||
func NewRecallTool(db *store.DB, provider ai.Provider, search config.SearchConfig, sessions *session.ActiveProjects) *RecallTool {
|
||||
return &RecallTool{store: db, provider: provider, search: search, sessions: sessions}
|
||||
}
|
||||
|
||||
func (t *RecallTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in RecallInput) (*mcp.CallToolResult, RecallOutput, error) {
|
||||
query := strings.TrimSpace(in.Query)
|
||||
if query == "" {
|
||||
return nil, RecallOutput{}, errInvalidInput("query is required")
|
||||
}
|
||||
|
||||
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false)
|
||||
if err != nil {
|
||||
return nil, RecallOutput{}, err
|
||||
}
|
||||
|
||||
limit := normalizeLimit(in.Limit, t.search)
|
||||
embedding, err := t.provider.Embed(ctx, query)
|
||||
if err != nil {
|
||||
return nil, RecallOutput{}, err
|
||||
}
|
||||
|
||||
var projectID *uuid.UUID
|
||||
if project != nil {
|
||||
projectID = &project.ID
|
||||
}
|
||||
|
||||
semantic, err := t.store.SearchSimilarThoughts(ctx, embedding, t.search.DefaultThreshold, limit, projectID, nil)
|
||||
if err != nil {
|
||||
return nil, RecallOutput{}, err
|
||||
}
|
||||
recent, err := t.store.RecentThoughts(ctx, projectID, limit, 0)
|
||||
if err != nil {
|
||||
return nil, RecallOutput{}, err
|
||||
}
|
||||
|
||||
items := make([]ContextItem, 0, limit*2)
|
||||
seen := map[string]struct{}{}
|
||||
for _, result := range semantic {
|
||||
key := result.ID.String()
|
||||
seen[key] = struct{}{}
|
||||
items = append(items, ContextItem{
|
||||
ID: key,
|
||||
Content: result.Content,
|
||||
Metadata: result.Metadata,
|
||||
Similarity: result.Similarity,
|
||||
Source: "semantic",
|
||||
})
|
||||
}
|
||||
for _, thought := range recent {
|
||||
key := thought.ID.String()
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
items = append(items, ContextItem{
|
||||
ID: key,
|
||||
Content: thought.Content,
|
||||
Metadata: thought.Metadata,
|
||||
Source: "recent",
|
||||
})
|
||||
}
|
||||
|
||||
lines := make([]string, 0, len(items))
|
||||
for i, item := range items {
|
||||
lines = append(lines, thoughtContextLine(i, item.Content, item.Metadata, item.Similarity))
|
||||
}
|
||||
header := "Recalled context"
|
||||
if project != nil {
|
||||
header = fmt.Sprintf("Recalled context for %s", project.Name)
|
||||
_ = t.store.TouchProject(ctx, project.ID)
|
||||
}
|
||||
|
||||
return nil, RecallOutput{
|
||||
Context: formatContextBlock(header, lines),
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
69
internal/tools/search.go
Normal file
69
internal/tools/search.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/ai"
|
||||
"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 SearchTool struct {
|
||||
store *store.DB
|
||||
provider ai.Provider
|
||||
search config.SearchConfig
|
||||
sessions *session.ActiveProjects
|
||||
}
|
||||
|
||||
type SearchInput struct {
|
||||
Query string `json:"query" jsonschema:"the semantic query to search for"`
|
||||
Limit int `json:"limit,omitempty" jsonschema:"maximum number of results to return"`
|
||||
Threshold float64 `json:"threshold,omitempty" jsonschema:"minimum similarity threshold between 0 and 1"`
|
||||
Project string `json:"project,omitempty" jsonschema:"optional project name or id to scope the search"`
|
||||
}
|
||||
|
||||
type SearchOutput struct {
|
||||
Results []thoughttypes.SearchResult `json:"results"`
|
||||
}
|
||||
|
||||
func NewSearchTool(db *store.DB, provider ai.Provider, search config.SearchConfig, sessions *session.ActiveProjects) *SearchTool {
|
||||
return &SearchTool{store: db, provider: provider, search: search, sessions: sessions}
|
||||
}
|
||||
|
||||
func (t *SearchTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in SearchInput) (*mcp.CallToolResult, SearchOutput, error) {
|
||||
query := strings.TrimSpace(in.Query)
|
||||
if query == "" {
|
||||
return nil, SearchOutput{}, errInvalidInput("query is required")
|
||||
}
|
||||
|
||||
limit := normalizeLimit(in.Limit, t.search)
|
||||
threshold := normalizeThreshold(in.Threshold, t.search.DefaultThreshold)
|
||||
|
||||
embedding, err := t.provider.Embed(ctx, query)
|
||||
if err != nil {
|
||||
return nil, SearchOutput{}, err
|
||||
}
|
||||
|
||||
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false)
|
||||
if err != nil {
|
||||
return nil, SearchOutput{}, err
|
||||
}
|
||||
|
||||
var results []thoughttypes.SearchResult
|
||||
if project != nil {
|
||||
results, err = t.store.SearchSimilarThoughts(ctx, embedding, threshold, limit, &project.ID, nil)
|
||||
_ = t.store.TouchProject(ctx, project.ID)
|
||||
} else {
|
||||
results, err = t.store.SearchThoughts(ctx, embedding, threshold, limit, map[string]any{})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, SearchOutput{}, err
|
||||
}
|
||||
|
||||
return nil, SearchOutput{Results: results}, nil
|
||||
}
|
||||
33
internal/tools/stats.go
Normal file
33
internal/tools/stats.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/store"
|
||||
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
||||
)
|
||||
|
||||
type StatsTool struct {
|
||||
store *store.DB
|
||||
}
|
||||
|
||||
type StatsInput struct{}
|
||||
|
||||
type StatsOutput struct {
|
||||
Stats thoughttypes.ThoughtStats `json:"stats"`
|
||||
}
|
||||
|
||||
func NewStatsTool(db *store.DB) *StatsTool {
|
||||
return &StatsTool{store: db}
|
||||
}
|
||||
|
||||
func (t *StatsTool) Handle(ctx context.Context, _ *mcp.CallToolRequest, _ StatsInput) (*mcp.CallToolResult, StatsOutput, error) {
|
||||
stats, err := t.store.Stats(ctx)
|
||||
if err != nil {
|
||||
return nil, StatsOutput{}, err
|
||||
}
|
||||
|
||||
return nil, StatsOutput{Stats: stats}, nil
|
||||
}
|
||||
93
internal/tools/summarize.go
Normal file
93
internal/tools/summarize.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/ai"
|
||||
"git.warky.dev/wdevs/amcs/internal/config"
|
||||
"git.warky.dev/wdevs/amcs/internal/session"
|
||||
"git.warky.dev/wdevs/amcs/internal/store"
|
||||
)
|
||||
|
||||
type SummarizeTool struct {
|
||||
store *store.DB
|
||||
provider ai.Provider
|
||||
search config.SearchConfig
|
||||
sessions *session.ActiveProjects
|
||||
}
|
||||
|
||||
type SummarizeInput struct {
|
||||
Query string `json:"query,omitempty" jsonschema:"optional semantic focus for the summary"`
|
||||
Project string `json:"project,omitempty" jsonschema:"optional project name or id; falls back to the active session project"`
|
||||
Days int `json:"days,omitempty" jsonschema:"only include thoughts from the last N days when query is omitted"`
|
||||
Limit int `json:"limit,omitempty" jsonschema:"maximum number of thoughts to summarize"`
|
||||
}
|
||||
|
||||
type SummarizeOutput struct {
|
||||
Summary string `json:"summary"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
func NewSummarizeTool(db *store.DB, provider ai.Provider, search config.SearchConfig, sessions *session.ActiveProjects) *SummarizeTool {
|
||||
return &SummarizeTool{store: db, provider: provider, search: search, sessions: sessions}
|
||||
}
|
||||
|
||||
func (t *SummarizeTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in SummarizeInput) (*mcp.CallToolResult, SummarizeOutput, error) {
|
||||
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false)
|
||||
if err != nil {
|
||||
return nil, SummarizeOutput{}, err
|
||||
}
|
||||
|
||||
limit := normalizeLimit(in.Limit, t.search)
|
||||
query := strings.TrimSpace(in.Query)
|
||||
lines := make([]string, 0, limit)
|
||||
count := 0
|
||||
|
||||
if query != "" {
|
||||
embedding, err := t.provider.Embed(ctx, query)
|
||||
if err != nil {
|
||||
return nil, SummarizeOutput{}, err
|
||||
}
|
||||
var projectID *uuid.UUID
|
||||
if project != nil {
|
||||
projectID = &project.ID
|
||||
}
|
||||
results, err := t.store.SearchSimilarThoughts(ctx, embedding, t.search.DefaultThreshold, limit, projectID, nil)
|
||||
if err != nil {
|
||||
return nil, SummarizeOutput{}, err
|
||||
}
|
||||
for i, result := range results {
|
||||
lines = append(lines, thoughtContextLine(i, result.Content, result.Metadata, result.Similarity))
|
||||
}
|
||||
count = len(results)
|
||||
} else {
|
||||
var projectID *uuid.UUID
|
||||
if project != nil {
|
||||
projectID = &project.ID
|
||||
}
|
||||
thoughts, err := t.store.RecentThoughts(ctx, projectID, limit, in.Days)
|
||||
if err != nil {
|
||||
return nil, SummarizeOutput{}, err
|
||||
}
|
||||
for i, thought := range thoughts {
|
||||
lines = append(lines, thoughtContextLine(i, thought.Content, thought.Metadata, 0))
|
||||
}
|
||||
count = len(thoughts)
|
||||
}
|
||||
|
||||
userPrompt := formatContextBlock("Summarize the following thoughts into concise prose with themes, action items, and notable people.", lines)
|
||||
systemPrompt := "You summarize note collections. Be concise, concrete, and structured in plain prose."
|
||||
summary, err := t.provider.Summarize(ctx, systemPrompt, userPrompt)
|
||||
if err != nil {
|
||||
return nil, SummarizeOutput{}, err
|
||||
}
|
||||
if project != nil {
|
||||
_ = t.store.TouchProject(ctx, project.ID)
|
||||
}
|
||||
|
||||
return nil, SummarizeOutput{Summary: summary, Count: count}, nil
|
||||
}
|
||||
88
internal/tools/update.go
Normal file
88
internal/tools/update.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/ai"
|
||||
"git.warky.dev/wdevs/amcs/internal/config"
|
||||
"git.warky.dev/wdevs/amcs/internal/metadata"
|
||||
"git.warky.dev/wdevs/amcs/internal/store"
|
||||
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
||||
)
|
||||
|
||||
type UpdateTool struct {
|
||||
store *store.DB
|
||||
provider ai.Provider
|
||||
capture config.CaptureConfig
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
type UpdateInput struct {
|
||||
ID string `json:"id" jsonschema:"the thought id"`
|
||||
Content *string `json:"content,omitempty" jsonschema:"replacement content for the thought"`
|
||||
Metadata thoughttypes.ThoughtMetadata `json:"metadata,omitempty" jsonschema:"metadata fields to merge into the thought"`
|
||||
Project string `json:"project,omitempty" jsonschema:"optional project name or id to move the thought into"`
|
||||
}
|
||||
|
||||
type UpdateOutput struct {
|
||||
Thought thoughttypes.Thought `json:"thought"`
|
||||
}
|
||||
|
||||
func NewUpdateTool(db *store.DB, provider ai.Provider, capture config.CaptureConfig, log *slog.Logger) *UpdateTool {
|
||||
return &UpdateTool{store: db, provider: provider, capture: capture, log: log}
|
||||
}
|
||||
|
||||
func (t *UpdateTool) Handle(ctx context.Context, _ *mcp.CallToolRequest, in UpdateInput) (*mcp.CallToolResult, UpdateOutput, error) {
|
||||
id, err := parseUUID(in.ID)
|
||||
if err != nil {
|
||||
return nil, UpdateOutput{}, err
|
||||
}
|
||||
|
||||
current, err := t.store.GetThought(ctx, id)
|
||||
if err != nil {
|
||||
return nil, UpdateOutput{}, err
|
||||
}
|
||||
|
||||
content := current.Content
|
||||
embedding := current.Embedding
|
||||
mergedMetadata := current.Metadata
|
||||
projectID := current.ProjectID
|
||||
|
||||
if in.Content != nil {
|
||||
content = strings.TrimSpace(*in.Content)
|
||||
if content == "" {
|
||||
return nil, UpdateOutput{}, errInvalidInput("content must not be empty")
|
||||
}
|
||||
embedding, err = t.provider.Embed(ctx, content)
|
||||
if err != nil {
|
||||
return nil, UpdateOutput{}, err
|
||||
}
|
||||
extracted, extractErr := t.provider.ExtractMetadata(ctx, content)
|
||||
if extractErr != nil {
|
||||
t.log.Warn("metadata extraction failed during update, keeping current metadata", slog.String("error", extractErr.Error()))
|
||||
} else {
|
||||
mergedMetadata = metadata.Normalize(extracted, t.capture)
|
||||
}
|
||||
}
|
||||
|
||||
mergedMetadata = metadata.Merge(mergedMetadata, in.Metadata, t.capture)
|
||||
|
||||
if rawProject := strings.TrimSpace(in.Project); rawProject != "" {
|
||||
project, err := t.store.GetProject(ctx, rawProject)
|
||||
if err != nil {
|
||||
return nil, UpdateOutput{}, err
|
||||
}
|
||||
projectID = &project.ID
|
||||
}
|
||||
|
||||
updated, err := t.store.UpdateThought(ctx, id, content, embedding, mergedMetadata, projectID)
|
||||
if err != nil {
|
||||
return nil, UpdateOutput{}, err
|
||||
}
|
||||
|
||||
return nil, UpdateOutput{Thought: updated}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user