feat(tools): add maintenance and meal planning tools with CRUD operations
- Implement maintenance tool for adding, logging, and retrieving tasks - Create meals tool for managing recipes, meal plans, and shopping lists - Introduce reparse metadata tool for updating thought metadata - Add household knowledge, home maintenance, family calendar, meal planning, and professional CRM database migrations - Grant necessary permissions for new database tables
This commit is contained in:
232
internal/tools/crm.go
Normal file
232
internal/tools/crm.go
Normal file
@@ -0,0 +1,232 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/store"
|
||||
ext "git.warky.dev/wdevs/amcs/internal/types"
|
||||
)
|
||||
|
||||
type CRMTool struct {
|
||||
store *store.DB
|
||||
}
|
||||
|
||||
func NewCRMTool(db *store.DB) *CRMTool {
|
||||
return &CRMTool{store: db}
|
||||
}
|
||||
|
||||
// add_professional_contact
|
||||
|
||||
type AddContactInput struct {
|
||||
Name string `json:"name" jsonschema:"contact's full name"`
|
||||
Company string `json:"company,omitempty"`
|
||||
Title string `json:"title,omitempty" jsonschema:"job title"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
LinkedInURL string `json:"linkedin_url,omitempty"`
|
||||
HowWeMet string `json:"how_we_met,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
FollowUpDate *time.Time `json:"follow_up_date,omitempty"`
|
||||
}
|
||||
|
||||
type AddContactOutput struct {
|
||||
Contact ext.ProfessionalContact `json:"contact"`
|
||||
}
|
||||
|
||||
func (t *CRMTool) AddContact(ctx context.Context, _ *mcp.CallToolRequest, in AddContactInput) (*mcp.CallToolResult, AddContactOutput, error) {
|
||||
if strings.TrimSpace(in.Name) == "" {
|
||||
return nil, AddContactOutput{}, errInvalidInput("name is required")
|
||||
}
|
||||
if in.Tags == nil {
|
||||
in.Tags = []string{}
|
||||
}
|
||||
contact, err := t.store.AddProfessionalContact(ctx, ext.ProfessionalContact{
|
||||
Name: strings.TrimSpace(in.Name),
|
||||
Company: strings.TrimSpace(in.Company),
|
||||
Title: strings.TrimSpace(in.Title),
|
||||
Email: strings.TrimSpace(in.Email),
|
||||
Phone: strings.TrimSpace(in.Phone),
|
||||
LinkedInURL: strings.TrimSpace(in.LinkedInURL),
|
||||
HowWeMet: strings.TrimSpace(in.HowWeMet),
|
||||
Tags: in.Tags,
|
||||
Notes: strings.TrimSpace(in.Notes),
|
||||
FollowUpDate: in.FollowUpDate,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, AddContactOutput{}, err
|
||||
}
|
||||
return nil, AddContactOutput{Contact: contact}, nil
|
||||
}
|
||||
|
||||
// search_contacts
|
||||
|
||||
type SearchContactsInput struct {
|
||||
Query string `json:"query,omitempty" jsonschema:"search text matching name, company, title, or notes"`
|
||||
Tags []string `json:"tags,omitempty" jsonschema:"filter by tags (all must match)"`
|
||||
}
|
||||
|
||||
type SearchContactsOutput struct {
|
||||
Contacts []ext.ProfessionalContact `json:"contacts"`
|
||||
}
|
||||
|
||||
func (t *CRMTool) SearchContacts(ctx context.Context, _ *mcp.CallToolRequest, in SearchContactsInput) (*mcp.CallToolResult, SearchContactsOutput, error) {
|
||||
contacts, err := t.store.SearchContacts(ctx, in.Query, in.Tags)
|
||||
if err != nil {
|
||||
return nil, SearchContactsOutput{}, err
|
||||
}
|
||||
if contacts == nil {
|
||||
contacts = []ext.ProfessionalContact{}
|
||||
}
|
||||
return nil, SearchContactsOutput{Contacts: contacts}, nil
|
||||
}
|
||||
|
||||
// log_interaction
|
||||
|
||||
type LogInteractionInput struct {
|
||||
ContactID uuid.UUID `json:"contact_id" jsonschema:"id of the contact"`
|
||||
InteractionType string `json:"interaction_type" jsonschema:"one of: meeting, email, call, coffee, event, linkedin, other"`
|
||||
OccurredAt *time.Time `json:"occurred_at,omitempty" jsonschema:"when it happened (defaults to now)"`
|
||||
Summary string `json:"summary" jsonschema:"summary of the interaction"`
|
||||
FollowUpNeeded bool `json:"follow_up_needed,omitempty"`
|
||||
FollowUpNotes string `json:"follow_up_notes,omitempty"`
|
||||
}
|
||||
|
||||
type LogInteractionOutput struct {
|
||||
Interaction ext.ContactInteraction `json:"interaction"`
|
||||
}
|
||||
|
||||
func (t *CRMTool) LogInteraction(ctx context.Context, _ *mcp.CallToolRequest, in LogInteractionInput) (*mcp.CallToolResult, LogInteractionOutput, error) {
|
||||
if strings.TrimSpace(in.Summary) == "" {
|
||||
return nil, LogInteractionOutput{}, errInvalidInput("summary is required")
|
||||
}
|
||||
occurredAt := time.Now()
|
||||
if in.OccurredAt != nil {
|
||||
occurredAt = *in.OccurredAt
|
||||
}
|
||||
interaction, err := t.store.LogInteraction(ctx, ext.ContactInteraction{
|
||||
ContactID: in.ContactID,
|
||||
InteractionType: in.InteractionType,
|
||||
OccurredAt: occurredAt,
|
||||
Summary: strings.TrimSpace(in.Summary),
|
||||
FollowUpNeeded: in.FollowUpNeeded,
|
||||
FollowUpNotes: strings.TrimSpace(in.FollowUpNotes),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, LogInteractionOutput{}, err
|
||||
}
|
||||
return nil, LogInteractionOutput{Interaction: interaction}, nil
|
||||
}
|
||||
|
||||
// get_contact_history
|
||||
|
||||
type GetContactHistoryInput struct {
|
||||
ContactID uuid.UUID `json:"contact_id" jsonschema:"id of the contact"`
|
||||
}
|
||||
|
||||
type GetContactHistoryOutput struct {
|
||||
History ext.ContactHistory `json:"history"`
|
||||
}
|
||||
|
||||
func (t *CRMTool) GetHistory(ctx context.Context, _ *mcp.CallToolRequest, in GetContactHistoryInput) (*mcp.CallToolResult, GetContactHistoryOutput, error) {
|
||||
history, err := t.store.GetContactHistory(ctx, in.ContactID)
|
||||
if err != nil {
|
||||
return nil, GetContactHistoryOutput{}, err
|
||||
}
|
||||
return nil, GetContactHistoryOutput{History: history}, nil
|
||||
}
|
||||
|
||||
// create_opportunity
|
||||
|
||||
type CreateOpportunityInput struct {
|
||||
ContactID *uuid.UUID `json:"contact_id,omitempty"`
|
||||
Title string `json:"title" jsonschema:"opportunity title"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Stage string `json:"stage,omitempty" jsonschema:"one of: identified, in_conversation, proposal, negotiation, won, lost (default: identified)"`
|
||||
Value *float64 `json:"value,omitempty" jsonschema:"monetary value"`
|
||||
ExpectedCloseDate *time.Time `json:"expected_close_date,omitempty"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
}
|
||||
|
||||
type CreateOpportunityOutput struct {
|
||||
Opportunity ext.Opportunity `json:"opportunity"`
|
||||
}
|
||||
|
||||
func (t *CRMTool) CreateOpportunity(ctx context.Context, _ *mcp.CallToolRequest, in CreateOpportunityInput) (*mcp.CallToolResult, CreateOpportunityOutput, error) {
|
||||
if strings.TrimSpace(in.Title) == "" {
|
||||
return nil, CreateOpportunityOutput{}, errInvalidInput("title is required")
|
||||
}
|
||||
stage := strings.TrimSpace(in.Stage)
|
||||
if stage == "" {
|
||||
stage = "identified"
|
||||
}
|
||||
opp, err := t.store.CreateOpportunity(ctx, ext.Opportunity{
|
||||
ContactID: in.ContactID,
|
||||
Title: strings.TrimSpace(in.Title),
|
||||
Description: strings.TrimSpace(in.Description),
|
||||
Stage: stage,
|
||||
Value: in.Value,
|
||||
ExpectedCloseDate: in.ExpectedCloseDate,
|
||||
Notes: strings.TrimSpace(in.Notes),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, CreateOpportunityOutput{}, err
|
||||
}
|
||||
return nil, CreateOpportunityOutput{Opportunity: opp}, nil
|
||||
}
|
||||
|
||||
// get_follow_ups_due
|
||||
|
||||
type GetFollowUpsDueInput struct {
|
||||
DaysAhead int `json:"days_ahead,omitempty" jsonschema:"look ahead window in days (default: 7)"`
|
||||
}
|
||||
|
||||
type GetFollowUpsDueOutput struct {
|
||||
Contacts []ext.ProfessionalContact `json:"contacts"`
|
||||
}
|
||||
|
||||
func (t *CRMTool) GetFollowUpsDue(ctx context.Context, _ *mcp.CallToolRequest, in GetFollowUpsDueInput) (*mcp.CallToolResult, GetFollowUpsDueOutput, error) {
|
||||
contacts, err := t.store.GetFollowUpsDue(ctx, in.DaysAhead)
|
||||
if err != nil {
|
||||
return nil, GetFollowUpsDueOutput{}, err
|
||||
}
|
||||
if contacts == nil {
|
||||
contacts = []ext.ProfessionalContact{}
|
||||
}
|
||||
return nil, GetFollowUpsDueOutput{Contacts: contacts}, nil
|
||||
}
|
||||
|
||||
// link_thought_to_contact
|
||||
|
||||
type LinkThoughtToContactInput struct {
|
||||
ContactID uuid.UUID `json:"contact_id" jsonschema:"id of the contact"`
|
||||
ThoughtID uuid.UUID `json:"thought_id" jsonschema:"id of the thought to link"`
|
||||
}
|
||||
|
||||
type LinkThoughtToContactOutput struct {
|
||||
Contact ext.ProfessionalContact `json:"contact"`
|
||||
}
|
||||
|
||||
func (t *CRMTool) LinkThought(ctx context.Context, _ *mcp.CallToolRequest, in LinkThoughtToContactInput) (*mcp.CallToolResult, LinkThoughtToContactOutput, error) {
|
||||
thought, err := t.store.GetThought(ctx, in.ThoughtID)
|
||||
if err != nil {
|
||||
return nil, LinkThoughtToContactOutput{}, fmt.Errorf("thought not found: %w", err)
|
||||
}
|
||||
|
||||
appendText := fmt.Sprintf("\n\n[Linked thought %s]: %s", thought.ID, thought.Content)
|
||||
if err := t.store.AppendThoughtToContactNotes(ctx, in.ContactID, appendText); err != nil {
|
||||
return nil, LinkThoughtToContactOutput{}, err
|
||||
}
|
||||
|
||||
contact, err := t.store.GetContact(ctx, in.ContactID)
|
||||
if err != nil {
|
||||
return nil, LinkThoughtToContactOutput{}, err
|
||||
}
|
||||
return nil, LinkThoughtToContactOutput{Contact: contact}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user