* Implement tests for error functions like errRequiredField, errInvalidField, and errEntityNotFound. * Ensure proper metadata is returned for various error scenarios. * Validate error handling in CRM, Files, and other tools. * Introduce tests for parsing stored file IDs and UUIDs. * Enhance coverage for helper functions related to project resolution and session management.
241 lines
8.2 KiB
Go
241 lines
8.2 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
"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{}, errRequiredField("name")
|
|
}
|
|
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{}, errRequiredField("summary")
|
|
}
|
|
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{}, errRequiredField("title")
|
|
}
|
|
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 {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, LinkThoughtToContactOutput{}, errEntityNotFound("thought", "thought_id", in.ThoughtID.String())
|
|
}
|
|
return nil, LinkThoughtToContactOutput{}, 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 {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, LinkThoughtToContactOutput{}, errEntityNotFound("contact", "contact_id", in.ContactID.String())
|
|
}
|
|
return nil, LinkThoughtToContactOutput{}, err
|
|
}
|
|
return nil, LinkThoughtToContactOutput{Contact: contact}, nil
|
|
}
|