mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-24 20:28:16 +02:00
make strictness of note creation inferred from the data
This commit is contained in:
parent
c21f976062
commit
76e3a2def9
10 changed files with 2463 additions and 8 deletions
|
|
@ -5,7 +5,7 @@
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rm -rf dist && tsc && mkdir -p dist/knowledge dist/pre_built && cp src/knowledge/note_creation.md dist/knowledge/ && cp src/pre_built/*.md dist/pre_built/",
|
"build": "rm -rf dist && tsc && mkdir -p dist/knowledge dist/pre_built && cp src/knowledge/note_creation_*.md dist/knowledge/ && cp src/pre_built/*.md dist/pre_built/",
|
||||||
"dev": "tsc -w"
|
"dev": "tsc -w"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { jsonSchema, ModelMessage } from "ai";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { WorkDir } from "../config/config.js";
|
import { WorkDir } from "../config/config.js";
|
||||||
|
import { getNoteCreationStrictness } from "../config/note_creation_config.js";
|
||||||
import { Agent, ToolAttachment } from "@x/shared/dist/agent.js";
|
import { Agent, ToolAttachment } from "@x/shared/dist/agent.js";
|
||||||
import { AssistantContentPart, AssistantMessage, Message, MessageList, ProviderOptions, ToolCallPart, ToolMessage } from "@x/shared/dist/message.js";
|
import { AssistantContentPart, AssistantMessage, Message, MessageList, ProviderOptions, ToolCallPart, ToolMessage } from "@x/shared/dist/message.js";
|
||||||
import { LanguageModel, stepCountIs, streamText, tool, Tool, ToolSet } from "ai";
|
import { LanguageModel, stepCountIs, streamText, tool, Tool, ToolSet } from "ai";
|
||||||
|
|
@ -245,16 +246,24 @@ export async function loadAgent(id: string): Promise<z.infer<typeof Agent>> {
|
||||||
return CopilotAgent;
|
return CopilotAgent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special case: load built-in agents from checked-in files
|
// Built-in agents loaded from checked-in files
|
||||||
const builtinAgents: Record<string, string> = {
|
const builtinAgents: Record<string, string> = {
|
||||||
'note_creation': '../knowledge/note_creation.md',
|
|
||||||
'meeting-prep': '../pre_built/meeting-prep.md',
|
'meeting-prep': '../pre_built/meeting-prep.md',
|
||||||
'email-draft': '../pre_built/email-draft.md',
|
'email-draft': '../pre_built/email-draft.md',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (id in builtinAgents) {
|
// Resolve agent file path (note_creation is dynamic based on strictness config)
|
||||||
const currentDir = path.dirname(new URL(import.meta.url).pathname);
|
let agentFilePath: string | null = null;
|
||||||
const agentFilePath = path.join(currentDir, builtinAgents[id]);
|
const currentDir = path.dirname(new URL(import.meta.url).pathname);
|
||||||
|
|
||||||
|
if (id === 'note_creation') {
|
||||||
|
const strictness = getNoteCreationStrictness();
|
||||||
|
agentFilePath = path.join(currentDir, `../knowledge/note_creation_${strictness}.md`);
|
||||||
|
} else if (id in builtinAgents) {
|
||||||
|
agentFilePath = path.join(currentDir, builtinAgents[id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agentFilePath) {
|
||||||
const raw = fs.readFileSync(agentFilePath, "utf8");
|
const raw = fs.readFileSync(agentFilePath, "utf8");
|
||||||
|
|
||||||
let agent: z.infer<typeof Agent> = {
|
let agent: z.infer<typeof Agent> = {
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,16 @@ function ensureDirs() {
|
||||||
ensure(path.join(WorkDir, "knowledge"));
|
ensure(path.join(WorkDir, "knowledge"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureDefaultConfigs() {
|
||||||
|
// Create note_creation.json with default strictness if it doesn't exist
|
||||||
|
const noteCreationConfig = path.join(WorkDir, "config", "note_creation.json");
|
||||||
|
if (!fs.existsSync(noteCreationConfig)) {
|
||||||
|
fs.writeFileSync(noteCreationConfig, JSON.stringify({
|
||||||
|
strictness: "high",
|
||||||
|
configured: false
|
||||||
|
}, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ensureDirs();
|
ensureDirs();
|
||||||
|
ensureDefaultConfigs();
|
||||||
94
apps/x/packages/core/src/config/note_creation_config.ts
Normal file
94
apps/x/packages/core/src/config/note_creation_config.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { WorkDir } from './config.js';
|
||||||
|
|
||||||
|
export type NoteCreationStrictness = 'low' | 'medium' | 'high';
|
||||||
|
|
||||||
|
interface NoteCreationConfig {
|
||||||
|
strictness: NoteCreationStrictness;
|
||||||
|
configured: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONFIG_FILE = path.join(WorkDir, 'config', 'note_creation.json');
|
||||||
|
const DEFAULT_STRICTNESS: NoteCreationStrictness = 'high';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the full config file.
|
||||||
|
*/
|
||||||
|
function readConfig(): NoteCreationConfig {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(CONFIG_FILE)) {
|
||||||
|
return { strictness: DEFAULT_STRICTNESS, configured: false };
|
||||||
|
}
|
||||||
|
const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
||||||
|
const config = JSON.parse(raw);
|
||||||
|
return {
|
||||||
|
strictness: ['low', 'medium', 'high'].includes(config.strictness)
|
||||||
|
? config.strictness
|
||||||
|
: DEFAULT_STRICTNESS,
|
||||||
|
configured: config.configured === true,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { strictness: DEFAULT_STRICTNESS, configured: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the full config file.
|
||||||
|
*/
|
||||||
|
function writeConfig(config: NoteCreationConfig): void {
|
||||||
|
const configDir = path.dirname(CONFIG_FILE);
|
||||||
|
if (!fs.existsSync(configDir)) {
|
||||||
|
fs.mkdirSync(configDir, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current note creation strictness setting.
|
||||||
|
* Defaults to 'high' if config doesn't exist.
|
||||||
|
*/
|
||||||
|
export function getNoteCreationStrictness(): NoteCreationStrictness {
|
||||||
|
return readConfig().strictness;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the note creation strictness setting.
|
||||||
|
* Preserves the configured flag.
|
||||||
|
*/
|
||||||
|
export function setNoteCreationStrictness(strictness: NoteCreationStrictness): void {
|
||||||
|
const config = readConfig();
|
||||||
|
config.strictness = strictness;
|
||||||
|
writeConfig(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if strictness has been auto-configured based on email analysis.
|
||||||
|
*/
|
||||||
|
export function isStrictnessConfigured(): boolean {
|
||||||
|
return readConfig().configured;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark strictness as configured (after auto-analysis).
|
||||||
|
*/
|
||||||
|
export function markStrictnessConfigured(): void {
|
||||||
|
const config = readConfig();
|
||||||
|
config.configured = true;
|
||||||
|
writeConfig(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set strictness and mark as configured in one operation.
|
||||||
|
*/
|
||||||
|
export function setStrictnessAndMarkConfigured(strictness: NoteCreationStrictness): void {
|
||||||
|
writeConfig({ strictness, configured: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the agent file name suffix based on strictness.
|
||||||
|
*/
|
||||||
|
export function getNoteCreationAgentSuffix(): string {
|
||||||
|
const strictness = getNoteCreationStrictness();
|
||||||
|
return `note_creation_${strictness}`;
|
||||||
|
}
|
||||||
482
apps/x/packages/core/src/config/strictness_analyzer.ts
Normal file
482
apps/x/packages/core/src/config/strictness_analyzer.ts
Normal file
|
|
@ -0,0 +1,482 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { WorkDir } from './config.js';
|
||||||
|
import {
|
||||||
|
NoteCreationStrictness,
|
||||||
|
setStrictnessAndMarkConfigured,
|
||||||
|
isStrictnessConfigured,
|
||||||
|
} from './note_creation_config.js';
|
||||||
|
|
||||||
|
const GMAIL_SYNC_DIR = path.join(WorkDir, 'gmail_sync');
|
||||||
|
|
||||||
|
interface EmailInfo {
|
||||||
|
threadId: string;
|
||||||
|
subject: string;
|
||||||
|
senders: string[];
|
||||||
|
senderEmails: string[];
|
||||||
|
body: string;
|
||||||
|
date: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AnalysisResult {
|
||||||
|
totalEmails: number;
|
||||||
|
uniqueSenders: number;
|
||||||
|
newsletterCount: number;
|
||||||
|
automatedCount: number;
|
||||||
|
consumerServiceCount: number;
|
||||||
|
businessCount: number;
|
||||||
|
mediumWouldCreate: number;
|
||||||
|
lowWouldCreate: number;
|
||||||
|
recommendation: NoteCreationStrictness;
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common newsletter/marketing patterns
|
||||||
|
const NEWSLETTER_PATTERNS = [
|
||||||
|
/unsubscribe/i,
|
||||||
|
/opt[- ]?out/i,
|
||||||
|
/email preferences/i,
|
||||||
|
/manage.*subscription/i,
|
||||||
|
/via sendgrid/i,
|
||||||
|
/via mailchimp/i,
|
||||||
|
/via hubspot/i,
|
||||||
|
/via constantcontact/i,
|
||||||
|
/list-unsubscribe/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
const NEWSLETTER_SENDER_PATTERNS = [
|
||||||
|
/^noreply@/i,
|
||||||
|
/^no-reply@/i,
|
||||||
|
/^newsletter@/i,
|
||||||
|
/^marketing@/i,
|
||||||
|
/^hello@/i,
|
||||||
|
/^info@/i,
|
||||||
|
/^team@/i,
|
||||||
|
/^updates@/i,
|
||||||
|
/^news@/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Automated/transactional patterns
|
||||||
|
const AUTOMATED_PATTERNS = [
|
||||||
|
/^notifications?@/i,
|
||||||
|
/^alerts?@/i,
|
||||||
|
/^support@/i,
|
||||||
|
/^billing@/i,
|
||||||
|
/^receipts?@/i,
|
||||||
|
/^orders?@/i,
|
||||||
|
/^shipping@/i,
|
||||||
|
/^noreply@/i,
|
||||||
|
/^donotreply@/i,
|
||||||
|
/^mailer-daemon/i,
|
||||||
|
/^postmaster@/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
const AUTOMATED_SUBJECT_PATTERNS = [
|
||||||
|
/password reset/i,
|
||||||
|
/verify your email/i,
|
||||||
|
/login alert/i,
|
||||||
|
/security alert/i,
|
||||||
|
/your order/i,
|
||||||
|
/order confirmation/i,
|
||||||
|
/shipping confirmation/i,
|
||||||
|
/receipt for/i,
|
||||||
|
/invoice/i,
|
||||||
|
/payment received/i,
|
||||||
|
/\[GitHub\]/i,
|
||||||
|
/\[Jira\]/i,
|
||||||
|
/\[Slack\]/i,
|
||||||
|
/\[Linear\]/i,
|
||||||
|
/\[Notion\]/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Consumer service domains (not business-relevant)
|
||||||
|
const CONSUMER_SERVICE_DOMAINS = [
|
||||||
|
'amazon.com', 'amazon.co.uk',
|
||||||
|
'netflix.com',
|
||||||
|
'spotify.com',
|
||||||
|
'uber.com', 'ubereats.com',
|
||||||
|
'doordash.com', 'grubhub.com',
|
||||||
|
'apple.com', 'apple.id',
|
||||||
|
'google.com', 'youtube.com',
|
||||||
|
'facebook.com', 'meta.com', 'instagram.com',
|
||||||
|
'twitter.com', 'x.com',
|
||||||
|
'linkedin.com',
|
||||||
|
'dropbox.com',
|
||||||
|
'paypal.com', 'venmo.com',
|
||||||
|
'chase.com', 'bankofamerica.com', 'wellsfargo.com', 'citi.com',
|
||||||
|
'att.com', 'verizon.com', 't-mobile.com',
|
||||||
|
'comcast.com', 'xfinity.com',
|
||||||
|
'delta.com', 'united.com', 'southwest.com', 'aa.com',
|
||||||
|
'airbnb.com', 'vrbo.com',
|
||||||
|
'walmart.com', 'target.com', 'bestbuy.com',
|
||||||
|
'costco.com',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a synced email markdown file
|
||||||
|
*/
|
||||||
|
function parseEmailFile(filePath: string): EmailInfo | null {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
// Extract subject from first heading
|
||||||
|
const subjectLine = lines.find(l => l.startsWith('# '));
|
||||||
|
const subject = subjectLine ? subjectLine.slice(2).trim() : '';
|
||||||
|
|
||||||
|
// Extract thread ID
|
||||||
|
const threadIdLine = lines.find(l => l.startsWith('**Thread ID:**'));
|
||||||
|
const threadId = threadIdLine ? threadIdLine.replace('**Thread ID:**', '').trim() : path.basename(filePath, '.md');
|
||||||
|
|
||||||
|
// Extract all senders
|
||||||
|
const senders: string[] = [];
|
||||||
|
const senderEmails: string[] = [];
|
||||||
|
let latestDate: Date | null = null;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('### From:')) {
|
||||||
|
const from = line.replace('### From:', '').trim();
|
||||||
|
senders.push(from);
|
||||||
|
|
||||||
|
// Extract email from "Name <email@domain.com>" format
|
||||||
|
const emailMatch = from.match(/<([^>]+)>/) || from.match(/([^\s<]+@[^\s>]+)/);
|
||||||
|
if (emailMatch) {
|
||||||
|
senderEmails.push(emailMatch[1].toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (line.startsWith('**Date:**')) {
|
||||||
|
const dateStr = line.replace('**Date:**', '').trim();
|
||||||
|
try {
|
||||||
|
const parsed = new Date(dateStr);
|
||||||
|
if (!isNaN(parsed.getTime())) {
|
||||||
|
if (!latestDate || parsed > latestDate) {
|
||||||
|
latestDate = parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore parse errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
threadId,
|
||||||
|
subject,
|
||||||
|
senders,
|
||||||
|
senderEmails,
|
||||||
|
body: content,
|
||||||
|
date: latestDate,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error parsing email file ${filePath}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if email is a newsletter/mass email
|
||||||
|
*/
|
||||||
|
function isNewsletter(email: EmailInfo): boolean {
|
||||||
|
// Check sender patterns
|
||||||
|
for (const senderEmail of email.senderEmails) {
|
||||||
|
for (const pattern of NEWSLETTER_SENDER_PATTERNS) {
|
||||||
|
if (pattern.test(senderEmail)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check body for unsubscribe patterns
|
||||||
|
for (const pattern of NEWSLETTER_PATTERNS) {
|
||||||
|
if (pattern.test(email.body)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if email is automated/transactional
|
||||||
|
*/
|
||||||
|
function isAutomated(email: EmailInfo): boolean {
|
||||||
|
// Check sender patterns
|
||||||
|
for (const senderEmail of email.senderEmails) {
|
||||||
|
for (const pattern of AUTOMATED_PATTERNS) {
|
||||||
|
if (pattern.test(senderEmail)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check subject patterns
|
||||||
|
for (const pattern of AUTOMATED_SUBJECT_PATTERNS) {
|
||||||
|
if (pattern.test(email.subject)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if email is from a consumer service
|
||||||
|
*/
|
||||||
|
function isConsumerService(email: EmailInfo): boolean {
|
||||||
|
for (const senderEmail of email.senderEmails) {
|
||||||
|
const domain = senderEmail.split('@')[1];
|
||||||
|
if (domain) {
|
||||||
|
// Check exact match or subdomain match (e.g., mail.amazon.com)
|
||||||
|
for (const consumerDomain of CONSUMER_SERVICE_DOMAINS) {
|
||||||
|
if (domain === consumerDomain || domain.endsWith(`.${consumerDomain}`)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Categorize an email based on its characteristics.
|
||||||
|
* Returns the category which determines how different strictness levels would handle it.
|
||||||
|
*/
|
||||||
|
type EmailCategory = 'internal' | 'newsletter' | 'automated' | 'consumer_service' | 'business';
|
||||||
|
|
||||||
|
function categorizeEmail(email: EmailInfo, userDomain: string): {
|
||||||
|
category: EmailCategory;
|
||||||
|
externalSenders: string[];
|
||||||
|
} {
|
||||||
|
// Filter out user's own domain
|
||||||
|
const externalSenders = email.senderEmails.filter(e => !e.endsWith(`@${userDomain}`));
|
||||||
|
if (externalSenders.length === 0) {
|
||||||
|
return { category: 'internal', externalSenders: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNewsletter(email)) {
|
||||||
|
return { category: 'newsletter', externalSenders };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAutomated(email)) {
|
||||||
|
return { category: 'automated', externalSenders };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isConsumerService(email)) {
|
||||||
|
return { category: 'consumer_service', externalSenders };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { category: 'business', externalSenders };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Infer user's domain from email patterns.
|
||||||
|
* Looks for the most common sender domain that appears frequently,
|
||||||
|
* assuming the user's own emails would be the most common sender.
|
||||||
|
*/
|
||||||
|
function inferUserDomain(emails: EmailInfo[]): string {
|
||||||
|
const domainCounts = new Map<string, number>();
|
||||||
|
|
||||||
|
for (const email of emails) {
|
||||||
|
for (const senderEmail of email.senderEmails) {
|
||||||
|
const domain = senderEmail.split('@')[1];
|
||||||
|
if (domain) {
|
||||||
|
domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the most frequent domain (likely the user's domain)
|
||||||
|
let maxCount = 0;
|
||||||
|
let userDomain = '';
|
||||||
|
|
||||||
|
for (const [domain, count] of domainCounts) {
|
||||||
|
// Skip known consumer/service domains
|
||||||
|
const isConsumer = CONSUMER_SERVICE_DOMAINS.some(
|
||||||
|
d => domain === d || domain.endsWith(`.${d}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isConsumer && count > maxCount) {
|
||||||
|
maxCount = count;
|
||||||
|
userDomain = domain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback if we couldn't determine
|
||||||
|
return userDomain || 'example.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyze emails and recommend a strictness level based on email patterns.
|
||||||
|
*
|
||||||
|
* Strictness levels filter emails as follows:
|
||||||
|
* - High: Only creates notes from meetings, emails just update existing notes
|
||||||
|
* - Medium: Creates notes for business emails (filters out consumer services)
|
||||||
|
* - Low: Creates notes for any human sender (only filters newsletters/automated)
|
||||||
|
*/
|
||||||
|
export function analyzeEmailsAndRecommend(): AnalysisResult {
|
||||||
|
const emails: EmailInfo[] = [];
|
||||||
|
|
||||||
|
// Read all email files from gmail_sync
|
||||||
|
if (fs.existsSync(GMAIL_SYNC_DIR)) {
|
||||||
|
const files = fs.readdirSync(GMAIL_SYNC_DIR).filter(f => f.endsWith('.md'));
|
||||||
|
|
||||||
|
// Filter to last 30 days
|
||||||
|
const thirtyDaysAgo = new Date();
|
||||||
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(GMAIL_SYNC_DIR, file);
|
||||||
|
const email = parseEmailFile(filePath);
|
||||||
|
if (email) {
|
||||||
|
// Include if date is within 30 days or if we can't parse the date
|
||||||
|
if (!email.date || email.date >= thirtyDaysAgo) {
|
||||||
|
emails.push(email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userDomain = inferUserDomain(emails);
|
||||||
|
console.log(`[StrictnessAnalyzer] Inferred user domain: ${userDomain}`);
|
||||||
|
|
||||||
|
// Track unique senders by category
|
||||||
|
const uniqueSenders = new Set<string>();
|
||||||
|
const newsletterSenders = new Set<string>();
|
||||||
|
const automatedSenders = new Set<string>();
|
||||||
|
const consumerServiceSenders = new Set<string>();
|
||||||
|
const businessSenders = new Set<string>();
|
||||||
|
|
||||||
|
let newsletterCount = 0;
|
||||||
|
let automatedCount = 0;
|
||||||
|
let consumerServiceCount = 0;
|
||||||
|
let businessCount = 0;
|
||||||
|
|
||||||
|
for (const email of emails) {
|
||||||
|
const result = categorizeEmail(email, userDomain);
|
||||||
|
|
||||||
|
for (const sender of result.externalSenders) {
|
||||||
|
uniqueSenders.add(sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (result.category) {
|
||||||
|
case 'newsletter':
|
||||||
|
newsletterCount++;
|
||||||
|
for (const sender of result.externalSenders) newsletterSenders.add(sender);
|
||||||
|
break;
|
||||||
|
case 'automated':
|
||||||
|
automatedCount++;
|
||||||
|
for (const sender of result.externalSenders) automatedSenders.add(sender);
|
||||||
|
break;
|
||||||
|
case 'consumer_service':
|
||||||
|
consumerServiceCount++;
|
||||||
|
for (const sender of result.externalSenders) consumerServiceSenders.add(sender);
|
||||||
|
break;
|
||||||
|
case 'business':
|
||||||
|
businessCount++;
|
||||||
|
for (const sender of result.externalSenders) businessSenders.add(sender);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate what each strictness level would capture:
|
||||||
|
// - Low: business + consumer_service senders (all human, non-automated)
|
||||||
|
// - Medium: business senders only (filters consumer services)
|
||||||
|
// - High: none from emails (only meetings create notes)
|
||||||
|
const lowWouldCreate = businessSenders.size + consumerServiceSenders.size;
|
||||||
|
const mediumWouldCreate = businessSenders.size;
|
||||||
|
|
||||||
|
// Determine recommendation based on email patterns
|
||||||
|
let recommendation: NoteCreationStrictness;
|
||||||
|
let reason: string;
|
||||||
|
|
||||||
|
const totalHumanSenders = lowWouldCreate;
|
||||||
|
const noiseRatio = uniqueSenders.size > 0
|
||||||
|
? (newsletterSenders.size + automatedSenders.size) / uniqueSenders.size
|
||||||
|
: 0;
|
||||||
|
const consumerRatio = totalHumanSenders > 0
|
||||||
|
? consumerServiceSenders.size / totalHumanSenders
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if (totalHumanSenders > 100) {
|
||||||
|
// High volume of contacts - recommend high to avoid noise
|
||||||
|
recommendation = 'high';
|
||||||
|
reason = `High volume of contacts (${totalHumanSenders} potential). High strictness focuses on people you meet, avoiding email overload.`;
|
||||||
|
} else if (totalHumanSenders > 50) {
|
||||||
|
// Moderate volume - recommend medium
|
||||||
|
recommendation = 'medium';
|
||||||
|
reason = `Moderate contact volume (${totalHumanSenders}). Medium strictness captures business contacts (${mediumWouldCreate}) while filtering consumer services.`;
|
||||||
|
} else if (consumerRatio > 0.5) {
|
||||||
|
// Lots of consumer service emails - medium helps filter
|
||||||
|
recommendation = 'medium';
|
||||||
|
reason = `${Math.round(consumerRatio * 100)}% of emails are from consumer services. Medium strictness filters these to focus on business contacts.`;
|
||||||
|
} else if (totalHumanSenders < 30) {
|
||||||
|
// Low volume - comprehensive capture is manageable
|
||||||
|
recommendation = 'low';
|
||||||
|
reason = `Low contact volume (${totalHumanSenders}). Low strictness provides comprehensive capture without overwhelming.`;
|
||||||
|
} else {
|
||||||
|
recommendation = 'medium';
|
||||||
|
reason = `Medium strictness provides a good balance, capturing ${mediumWouldCreate} business contacts.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalEmails: emails.length,
|
||||||
|
uniqueSenders: uniqueSenders.size,
|
||||||
|
newsletterCount,
|
||||||
|
automatedCount,
|
||||||
|
consumerServiceCount,
|
||||||
|
businessCount,
|
||||||
|
mediumWouldCreate,
|
||||||
|
lowWouldCreate,
|
||||||
|
recommendation,
|
||||||
|
reason,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run analysis and auto-configure strictness if not already done.
|
||||||
|
* Returns true if configuration was updated.
|
||||||
|
*/
|
||||||
|
export function autoConfigureStrictnessIfNeeded(): boolean {
|
||||||
|
if (isStrictnessConfigured()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are any emails to analyze
|
||||||
|
if (!fs.existsSync(GMAIL_SYNC_DIR)) {
|
||||||
|
console.log('[StrictnessAnalyzer] No gmail_sync directory found, skipping auto-configuration');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailFiles = fs.readdirSync(GMAIL_SYNC_DIR).filter(f => f.endsWith('.md'));
|
||||||
|
if (emailFiles.length === 0) {
|
||||||
|
console.log('[StrictnessAnalyzer] No emails found to analyze, skipping auto-configuration');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need at least 10 emails for meaningful analysis
|
||||||
|
if (emailFiles.length < 10) {
|
||||||
|
console.log(`[StrictnessAnalyzer] Only ${emailFiles.length} emails found, need at least 10 for meaningful analysis. Using default 'high' strictness.`);
|
||||||
|
setStrictnessAndMarkConfigured('high');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[StrictnessAnalyzer] Running email analysis for auto-configuration...');
|
||||||
|
const result = analyzeEmailsAndRecommend();
|
||||||
|
|
||||||
|
console.log('[StrictnessAnalyzer] Analysis complete:');
|
||||||
|
console.log(` - Total emails analyzed: ${result.totalEmails}`);
|
||||||
|
console.log(` - Unique external senders: ${result.uniqueSenders}`);
|
||||||
|
console.log(` - Newsletters/mass emails: ${result.newsletterCount}`);
|
||||||
|
console.log(` - Automated/transactional: ${result.automatedCount}`);
|
||||||
|
console.log(` - Consumer services: ${result.consumerServiceCount}`);
|
||||||
|
console.log(` - Business emails: ${result.businessCount}`);
|
||||||
|
console.log(` - Medium strictness would capture: ${result.mediumWouldCreate} contacts`);
|
||||||
|
console.log(` - Low strictness would capture: ${result.lowWouldCreate} contacts`);
|
||||||
|
console.log(` - Recommendation: ${result.recommendation.toUpperCase()}`);
|
||||||
|
console.log(` - Reason: ${result.reason}`);
|
||||||
|
|
||||||
|
setStrictnessAndMarkConfigured(result.recommendation);
|
||||||
|
console.log(`[StrictnessAnalyzer] Auto-configured note creation strictness to: ${result.recommendation}`);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
@ -137,7 +137,82 @@ resetGraphState(); // Clears the state file
|
||||||
|
|
||||||
Or manually delete: `~/.rowboat/knowledge_graph_state.json`
|
Or manually delete: `~/.rowboat/knowledge_graph_state.json`
|
||||||
|
|
||||||
## Configuration
|
## Note Creation Strictness
|
||||||
|
|
||||||
|
The system supports three strictness levels that control how aggressively notes are created from emails. Meetings always create notes at all levels.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Strictness is configured in `~/.rowboat/config/note_creation.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"strictness": "medium",
|
||||||
|
"configured": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
On first run, the system auto-analyzes your emails and recommends a setting based on volume and patterns.
|
||||||
|
|
||||||
|
### Strictness Levels
|
||||||
|
|
||||||
|
| Level | Philosophy |
|
||||||
|
|-------|------------|
|
||||||
|
| **High** | "Meetings create notes. Emails enrich them." |
|
||||||
|
| **Medium** | "Both create notes, but emails require personalized content." |
|
||||||
|
| **Low** | "Capture broadly. Never miss a potentially important contact." |
|
||||||
|
|
||||||
|
### What Each Level Filters
|
||||||
|
|
||||||
|
| Email Type | High | Medium | Low |
|
||||||
|
|------------|------|--------|-----|
|
||||||
|
| Mass newsletters | Skip | Skip | Skip |
|
||||||
|
| Automated/system emails | Skip | Skip | Skip |
|
||||||
|
| Consumer services (Amazon, Netflix, banks) | Skip | Skip | ✅ Create |
|
||||||
|
| Generic cold sales | Skip | Skip | ✅ Create |
|
||||||
|
| Recruiters | Skip | Skip | ✅ Create |
|
||||||
|
| Support reps | Skip | Skip | ✅ Create |
|
||||||
|
| Personalized business emails | Skip | ✅ Create | ✅ Create |
|
||||||
|
| Warm intros | ✅ Create | ✅ Create | ✅ Create |
|
||||||
|
|
||||||
|
### High Strictness
|
||||||
|
|
||||||
|
- Emails **never create** new notes (only meetings do)
|
||||||
|
- Emails can only **update existing** notes for people you've already met
|
||||||
|
- Exception: Warm intros from known contacts can create notes
|
||||||
|
- Best for: Users who get lots of emails and want minimal noise
|
||||||
|
|
||||||
|
### Medium Strictness
|
||||||
|
|
||||||
|
- Emails **can create** notes if personalized and business-relevant
|
||||||
|
- Filters out consumer services, mass mail, generic pitches
|
||||||
|
- Warm intros from anyone (not just existing contacts) create notes
|
||||||
|
- Best for: Balanced capture of relevant business contacts
|
||||||
|
|
||||||
|
### Low Strictness
|
||||||
|
|
||||||
|
- Creates notes for **any identifiable human sender**
|
||||||
|
- Only skips obvious automated emails and newsletters
|
||||||
|
- Philosophy: "Better to have a note you don't need than to miss someone important"
|
||||||
|
- Best for: Users with low email volume who want comprehensive capture
|
||||||
|
|
||||||
|
### Auto-Configuration
|
||||||
|
|
||||||
|
On first run, `strictness_analyzer.ts` analyzes your emails and recommends a level:
|
||||||
|
|
||||||
|
- **>100 human senders** → Recommends High (avoid overload)
|
||||||
|
- **50-100 senders** → Recommends Medium (balanced)
|
||||||
|
- **>50% consumer services** → Recommends Medium (filter noise)
|
||||||
|
- **<30 senders** → Recommends Low (comprehensive capture is manageable)
|
||||||
|
|
||||||
|
### Prompt Files
|
||||||
|
|
||||||
|
Each strictness level has its own agent prompt:
|
||||||
|
- `note_creation_high.md` - Original strict rules
|
||||||
|
- `note_creation_medium.md` - Relaxed for personalized emails
|
||||||
|
- `note_creation_low.md` - Minimal filtering
|
||||||
|
|
||||||
|
## Other Configuration
|
||||||
|
|
||||||
### Batch Size
|
### Batch Size
|
||||||
Change `BATCH_SIZE` in `build_graph.ts` (currently 25 files per batch)
|
Change `BATCH_SIZE` in `build_graph.ts` (currently 25 files per batch)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { WorkDir } from '../config/config.js';
|
import { WorkDir } from '../config/config.js';
|
||||||
|
import { autoConfigureStrictnessIfNeeded } from '../config/strictness_analyzer.js';
|
||||||
import { createRun, createMessage } from '../runs/runs.js';
|
import { createRun, createMessage } from '../runs/runs.js';
|
||||||
import { bus } from '../runs/bus.js';
|
import { bus } from '../runs/bus.js';
|
||||||
import {
|
import {
|
||||||
|
|
@ -181,6 +182,9 @@ export async function buildGraph(sourceDir: string): Promise<void> {
|
||||||
async function processAllSources(): Promise<void> {
|
async function processAllSources(): Promise<void> {
|
||||||
console.log('[GraphBuilder] Checking for new content in all sources...');
|
console.log('[GraphBuilder] Checking for new content in all sources...');
|
||||||
|
|
||||||
|
// Auto-configure strictness on first run if not already done
|
||||||
|
autoConfigureStrictnessIfNeeded();
|
||||||
|
|
||||||
let anyFilesProcessed = false;
|
let anyFilesProcessed = false;
|
||||||
|
|
||||||
for (const folder of SOURCE_FOLDERS) {
|
for (const folder of SOURCE_FOLDERS) {
|
||||||
|
|
|
||||||
725
apps/x/packages/core/src/knowledge/note_creation_low.md
Normal file
725
apps/x/packages/core/src/knowledge/note_creation_low.md
Normal file
|
|
@ -0,0 +1,725 @@
|
||||||
|
---
|
||||||
|
model: gpt-5.2
|
||||||
|
tools:
|
||||||
|
workspace-writeFile:
|
||||||
|
type: builtin
|
||||||
|
name: workspace-writeFile
|
||||||
|
workspace-readFile:
|
||||||
|
type: builtin
|
||||||
|
name: workspace-readFile
|
||||||
|
workspace-readdir:
|
||||||
|
type: builtin
|
||||||
|
name: workspace-readdir
|
||||||
|
workspace-mkdir:
|
||||||
|
type: builtin
|
||||||
|
name: workspace-mkdir
|
||||||
|
executeCommand:
|
||||||
|
type: builtin
|
||||||
|
name: executeCommand
|
||||||
|
---
|
||||||
|
# Task
|
||||||
|
|
||||||
|
You are a memory agent. Given a single source file (email or meeting transcript), you will:
|
||||||
|
|
||||||
|
1. **Determine source type (meeting or email)**
|
||||||
|
2. **Evaluate if the source is worth processing**
|
||||||
|
3. **Search for all existing related notes**
|
||||||
|
4. **Resolve entities to canonical names**
|
||||||
|
5. Identify new entities worth tracking
|
||||||
|
6. Extract structured information (decisions, commitments, key facts)
|
||||||
|
7. **Detect state changes (status updates, resolved items, role changes)**
|
||||||
|
8. Create new notes or update existing notes
|
||||||
|
9. **Apply state changes to existing notes**
|
||||||
|
|
||||||
|
The core rule: **Capture broadly. Both meetings and emails create notes for most external contacts.**
|
||||||
|
|
||||||
|
You have full read access to the existing knowledge directory. Use this extensively to:
|
||||||
|
- Find existing notes for people, organizations, projects mentioned
|
||||||
|
- Resolve ambiguous names (find existing note for "David")
|
||||||
|
- Understand existing relationships before updating
|
||||||
|
- Avoid creating duplicate notes
|
||||||
|
- Maintain consistency with existing content
|
||||||
|
- **Detect when new information changes the state of existing notes**
|
||||||
|
|
||||||
|
# Inputs
|
||||||
|
|
||||||
|
1. **source_file**: Path to a single file to process (email or meeting transcript)
|
||||||
|
2. **knowledge_folder**: Path to Obsidian vault (read/write access)
|
||||||
|
3. **user**: Information about the owner of this memory
|
||||||
|
- name: e.g., "Arj"
|
||||||
|
- email: e.g., "arj@rowboat.com"
|
||||||
|
- domain: e.g., "rowboat.com"
|
||||||
|
|
||||||
|
# Tools Available
|
||||||
|
|
||||||
|
You have access to `executeCommand` to run shell commands:
|
||||||
|
```
|
||||||
|
executeCommand("ls {path}") # List directory contents
|
||||||
|
executeCommand("cat {path}") # Read file contents
|
||||||
|
executeCommand("grep -r '{pattern}' {path}") # Search across files
|
||||||
|
executeCommand("grep -r -l '{pattern}' {path}") # List files containing pattern
|
||||||
|
executeCommand("grep -r -i '{pattern}' {path}") # Case-insensitive search
|
||||||
|
executeCommand("head -50 {path}") # Read first 50 lines
|
||||||
|
executeCommand("write {path} {content}") # Create or overwrite file
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** Use shell escaping for paths with spaces:
|
||||||
|
```
|
||||||
|
executeCommand("cat 'knowledge_folder/People/Sarah Chen.md'")
|
||||||
|
executeCommand("grep -r 'David' 'knowledge_folder/People/'")
|
||||||
|
```
|
||||||
|
|
||||||
|
# Output
|
||||||
|
|
||||||
|
Either:
|
||||||
|
- **SKIP** with reason, if source should be ignored
|
||||||
|
- Updated or new markdown files in notes_folder
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# The Core Rule: Low Strictness - Capture Broadly
|
||||||
|
|
||||||
|
**LOW STRICTNESS MODE**
|
||||||
|
|
||||||
|
This mode prioritizes comprehensive capture over selectivity. The goal is to never miss a potentially important contact.
|
||||||
|
|
||||||
|
**Meetings create notes for:**
|
||||||
|
- All external attendees (anyone not @user.domain)
|
||||||
|
|
||||||
|
**Emails create notes for:**
|
||||||
|
- Any personalized email from an identifiable sender
|
||||||
|
- Anyone who reaches out directly
|
||||||
|
- Any external contact who communicates with you
|
||||||
|
|
||||||
|
**Only skip:**
|
||||||
|
- Obvious automated/system emails (no human sender)
|
||||||
|
- Mass newsletters with unsubscribe links
|
||||||
|
- Truly anonymous or unidentifiable senders
|
||||||
|
|
||||||
|
**Philosophy:** It's better to have a note you don't need than to miss tracking someone important.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 0: Determine Source Type
|
||||||
|
|
||||||
|
Read the source file and determine if it's a meeting or email.
|
||||||
|
```
|
||||||
|
executeCommand("cat '{source_file}'")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Meeting indicators:**
|
||||||
|
- Has `Attendees:` field
|
||||||
|
- Has `Meeting:` title
|
||||||
|
- Transcript format with speaker labels
|
||||||
|
- Calendar event metadata
|
||||||
|
|
||||||
|
**Email indicators:**
|
||||||
|
- Has `From:` and `To:` fields
|
||||||
|
- Has `Subject:` field
|
||||||
|
- Email signature
|
||||||
|
|
||||||
|
**Set processing mode:**
|
||||||
|
- `source_type = "meeting"` → Create notes for all external attendees
|
||||||
|
- `source_type = "email"` → Create notes for sender if identifiable human
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 1: Source Filtering (Minimal)
|
||||||
|
|
||||||
|
## Skip Only These Sources
|
||||||
|
|
||||||
|
### Mass Newsletters
|
||||||
|
|
||||||
|
**Indicators (must have MULTIPLE of these):**
|
||||||
|
- Unsubscribe link in body or footer
|
||||||
|
- From a marketing address (noreply@, newsletter@, marketing@)
|
||||||
|
- Sent to multiple recipients or undisclosed-recipients
|
||||||
|
- Sent via marketing platforms (via sendgrid, via mailchimp, etc.)
|
||||||
|
|
||||||
|
**Action:** SKIP with reason "Mass newsletter"
|
||||||
|
|
||||||
|
### Purely Automated (No Human Sender)
|
||||||
|
|
||||||
|
**Indicators:**
|
||||||
|
- From automated systems with no human behind them (alerts@, notifications@)
|
||||||
|
- Password resets, login alerts
|
||||||
|
- System notifications (GitHub automated, CI/CD alerts)
|
||||||
|
- Receipt confirmations with no human contact info
|
||||||
|
|
||||||
|
**Action:** SKIP with reason "Automated system message"
|
||||||
|
|
||||||
|
### Truly Low-Signal
|
||||||
|
|
||||||
|
**Indicators (must be clearly content-free):**
|
||||||
|
- Body is ONLY "Thanks!", "Got it", "OK" with nothing else
|
||||||
|
- Auto-replies ("I'm out of office") with no human context
|
||||||
|
|
||||||
|
**Action:** SKIP with reason "No substantive content"
|
||||||
|
|
||||||
|
## Process Everything Else
|
||||||
|
|
||||||
|
**Important:** When in doubt, PROCESS. In low strictness mode, we err on the side of capturing more.
|
||||||
|
|
||||||
|
If skipping:
|
||||||
|
```
|
||||||
|
SKIP
|
||||||
|
Reason: {reason}
|
||||||
|
```
|
||||||
|
|
||||||
|
If processing, continue to Step 2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 2: Read and Parse Source File
|
||||||
|
```
|
||||||
|
executeCommand("cat '{source_file}'")
|
||||||
|
```
|
||||||
|
|
||||||
|
Extract metadata:
|
||||||
|
|
||||||
|
**For meetings:**
|
||||||
|
- **Date:** From header or filename
|
||||||
|
- **Title:** Meeting name
|
||||||
|
- **Attendees:** List of participants
|
||||||
|
- **Duration:** If available
|
||||||
|
|
||||||
|
**For emails:**
|
||||||
|
- **Date:** From `Date:` header
|
||||||
|
- **Subject:** From `Subject:` header
|
||||||
|
- **From:** Sender email/name
|
||||||
|
- **To/Cc:** Recipients
|
||||||
|
|
||||||
|
## 2a: Exclude Self
|
||||||
|
|
||||||
|
Never create or update notes for:
|
||||||
|
- The user (matches user.name, user.email, or @user.domain)
|
||||||
|
- Anyone @{user.domain} (colleagues at user's company)
|
||||||
|
|
||||||
|
Filter these out from attendees/participants before proceeding.
|
||||||
|
|
||||||
|
## 2b: Extract All Name Variants
|
||||||
|
|
||||||
|
From the source, collect every way entities are referenced:
|
||||||
|
|
||||||
|
**People variants:**
|
||||||
|
- Full names: "Sarah Chen"
|
||||||
|
- First names only: "Sarah"
|
||||||
|
- Last names only: "Chen"
|
||||||
|
- Initials: "S. Chen"
|
||||||
|
- Email addresses: "sarah@acme.com"
|
||||||
|
- Roles/titles: "their CTO", "the VP of Engineering"
|
||||||
|
|
||||||
|
**Organization variants:**
|
||||||
|
- Full names: "Acme Corporation"
|
||||||
|
- Short names: "Acme"
|
||||||
|
- Abbreviations: "AC"
|
||||||
|
- Email domains: "@acme.com"
|
||||||
|
|
||||||
|
**Project variants:**
|
||||||
|
- Explicit names: "Project Atlas"
|
||||||
|
- Descriptive references: "the integration", "the pilot", "the deal"
|
||||||
|
|
||||||
|
Create a list of all variants found.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 3: Search for Existing Notes
|
||||||
|
|
||||||
|
For each variant identified, search the notes folder thoroughly.
|
||||||
|
|
||||||
|
## 3a: Search by People
|
||||||
|
```bash
|
||||||
|
executeCommand("grep -r -i -l 'Sarah Chen' '{knowledge_folder}/'")
|
||||||
|
executeCommand("grep -r -i -l 'Sarah' '{knowledge_folder}/People/'")
|
||||||
|
executeCommand("grep -r -i -l 'sarah@acme.com' '{knowledge_folder}/'")
|
||||||
|
executeCommand("grep -r -i -l '@acme.com' '{knowledge_folder}/'")
|
||||||
|
executeCommand("grep -r -i 'Aliases.*Sarah' '{knowledge_folder}/People/'")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3b: Search by Organizations
|
||||||
|
```bash
|
||||||
|
executeCommand("ls '{knowledge_folder}/Organizations/'")
|
||||||
|
executeCommand("grep -r -i -l 'Acme' '{knowledge_folder}/Organizations/'")
|
||||||
|
executeCommand("grep -r -i 'Domain.*acme.com' '{knowledge_folder}/Organizations/'")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3c: Search by Projects and Topics
|
||||||
|
```bash
|
||||||
|
executeCommand("ls '{knowledge_folder}/Projects/'")
|
||||||
|
executeCommand("grep -r -i 'Acme' '{knowledge_folder}/Projects/'")
|
||||||
|
executeCommand("ls '{knowledge_folder}/Topics/'")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3d: Read Candidate Notes
|
||||||
|
|
||||||
|
For every note file found in searches, read it to understand context.
|
||||||
|
|
||||||
|
## 3e: Matching Criteria
|
||||||
|
|
||||||
|
Use standard matching criteria for names, emails, and organizations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 4: Resolve Entities to Canonical Names
|
||||||
|
|
||||||
|
Using the search results from Step 3, resolve each variant to a canonical name.
|
||||||
|
|
||||||
|
## 4a: Build Resolution Map
|
||||||
|
|
||||||
|
Create a mapping from every source reference to its canonical form.
|
||||||
|
|
||||||
|
## 4b: Apply Source Type Rules (Low Strictness)
|
||||||
|
|
||||||
|
**If source_type == "meeting":**
|
||||||
|
- Resolved entities → Update existing notes
|
||||||
|
- New entities → Create new notes for ALL external attendees
|
||||||
|
|
||||||
|
**If source_type == "email" (LOW STRICTNESS):**
|
||||||
|
- Resolved entities → Update existing notes
|
||||||
|
- New entities → Create notes for the sender and any mentioned contacts
|
||||||
|
|
||||||
|
## 4c: Disambiguation Rules
|
||||||
|
|
||||||
|
When multiple candidates match a variant, disambiguate by:
|
||||||
|
1. Email match (definitive)
|
||||||
|
2. Organization context (strong signal)
|
||||||
|
3. Role match
|
||||||
|
4. Recency (tiebreaker)
|
||||||
|
|
||||||
|
## 4d: Resolution Map Output
|
||||||
|
|
||||||
|
Final resolution map before proceeding:
|
||||||
|
```
|
||||||
|
RESOLVED (use canonical name with absolute path):
|
||||||
|
- "Sarah", "Sarah Chen", "sarah@acme.com" → [[People/Sarah Chen]]
|
||||||
|
|
||||||
|
NEW ENTITIES (create notes):
|
||||||
|
- "Jennifer" (CTO, Acme Corp) → Create [[People/Jennifer]]
|
||||||
|
|
||||||
|
AMBIGUOUS (create with disambiguation note):
|
||||||
|
- "Mike" (no context) → Create [[People/Mike]] with note about ambiguity
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 5: Identify New Entities (Low Strictness - Capture Broadly)
|
||||||
|
|
||||||
|
For entities not resolved to existing notes, create notes for most of them.
|
||||||
|
|
||||||
|
## People
|
||||||
|
|
||||||
|
### Who Gets a Note (Low Strictness)
|
||||||
|
|
||||||
|
**CREATE a note for:**
|
||||||
|
- ALL external meeting attendees (not @user.domain)
|
||||||
|
- ALL email senders with identifiable names/emails
|
||||||
|
- Anyone CC'd on emails who seems relevant
|
||||||
|
- Anyone mentioned by name in conversations
|
||||||
|
- Cold outreach senders (even if unsolicited)
|
||||||
|
- Sales reps, recruiters, service providers
|
||||||
|
- Anyone who might be useful to remember later
|
||||||
|
|
||||||
|
**DO NOT create notes for:**
|
||||||
|
- Internal colleagues (@user.domain)
|
||||||
|
- Truly anonymous/unidentifiable senders
|
||||||
|
- System-generated sender names with no human behind them
|
||||||
|
|
||||||
|
### The Low Strictness Test
|
||||||
|
|
||||||
|
Ask: Could this person ever be useful to remember?
|
||||||
|
|
||||||
|
- Sarah Chen, VP Engineering → **Yes, create note**
|
||||||
|
- James from HSBC → **Yes, create note** (might need banking help again)
|
||||||
|
- Random recruiter → **Yes, create note** (might want to contact later)
|
||||||
|
- Cold sales person → **Yes, create note** (might be relevant someday)
|
||||||
|
- Support rep → **Yes, create note** (might need them again)
|
||||||
|
|
||||||
|
### Role Inference
|
||||||
|
|
||||||
|
If role is not explicitly stated, infer from context. Write "Unknown" only if truly impossible to infer anything.
|
||||||
|
|
||||||
|
### Relationship Type Guide (Low Strictness)
|
||||||
|
|
||||||
|
| Relationship Type | Create People Notes? | Create Org Note? |
|
||||||
|
|-------------------|----------------------|------------------|
|
||||||
|
| Customer | Yes — all contacts | Yes |
|
||||||
|
| Prospect | Yes — all contacts | Yes |
|
||||||
|
| Investor | Yes | Yes |
|
||||||
|
| Partner | Yes — all contacts | Yes |
|
||||||
|
| Vendor | Yes — all contacts | Yes |
|
||||||
|
| Bank/Financial | Yes | Yes |
|
||||||
|
| Candidate | Yes | No |
|
||||||
|
| Recruiter | Yes | Optional |
|
||||||
|
| Service provider | Yes | Optional |
|
||||||
|
| Cold outreach | Yes | Optional |
|
||||||
|
| Support interaction | Yes | Optional |
|
||||||
|
|
||||||
|
## Organizations
|
||||||
|
|
||||||
|
**CREATE a note if:**
|
||||||
|
- Anyone from that org is mentioned or contacted you
|
||||||
|
- The org is mentioned in any context
|
||||||
|
|
||||||
|
**Only skip:**
|
||||||
|
- Organizations you genuinely can't identify
|
||||||
|
|
||||||
|
## Projects
|
||||||
|
|
||||||
|
**CREATE a note if:**
|
||||||
|
- Discussed in meeting or email
|
||||||
|
- Any indication of ongoing work or collaboration
|
||||||
|
|
||||||
|
## Topics
|
||||||
|
|
||||||
|
**CREATE a note if:**
|
||||||
|
- Mentioned more than once
|
||||||
|
- Seems like a recurring theme
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 6: Extract Content
|
||||||
|
|
||||||
|
For each entity that has or will have a note, extract relevant content.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
Extract what was decided, when, by whom, and why.
|
||||||
|
|
||||||
|
## Commitments
|
||||||
|
|
||||||
|
Extract who committed to what, and any deadlines.
|
||||||
|
|
||||||
|
## Key Facts
|
||||||
|
|
||||||
|
Key facts should be **substantive information** — not commentary about missing data.
|
||||||
|
|
||||||
|
**Extract if:**
|
||||||
|
- Specific numbers, dates, or metrics
|
||||||
|
- Preferences or working style
|
||||||
|
- Background information
|
||||||
|
- Authority or decision process
|
||||||
|
- Concerns or constraints
|
||||||
|
- What they're working on or interested in
|
||||||
|
|
||||||
|
**Never include:**
|
||||||
|
- Meta-commentary about missing data
|
||||||
|
- Obvious facts already in Info section
|
||||||
|
- Placeholder text
|
||||||
|
|
||||||
|
**If there are no substantive key facts, leave the section empty.**
|
||||||
|
|
||||||
|
## Open Items
|
||||||
|
|
||||||
|
**Include:**
|
||||||
|
- Commitments made
|
||||||
|
- Requests received
|
||||||
|
- Next steps discussed
|
||||||
|
- Follow-ups agreed
|
||||||
|
|
||||||
|
**Never include:**
|
||||||
|
- Data gaps or research tasks
|
||||||
|
- Wishes or hypotheticals
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The summary should answer: **"Who is this person and why do I know them?"**
|
||||||
|
|
||||||
|
Write 2-3 sentences covering their role/function, context of the relationship, and what you're discussing.
|
||||||
|
|
||||||
|
## Activity Summary
|
||||||
|
|
||||||
|
One line summarizing this source's relevance to the entity:
|
||||||
|
```
|
||||||
|
**{YYYY-MM-DD}** ({meeting|email}): {Summary with [[links]]}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 7: Detect State Changes
|
||||||
|
|
||||||
|
Review the extracted content for signals that existing note fields should be updated.
|
||||||
|
|
||||||
|
## 7a: Project Status Changes
|
||||||
|
|
||||||
|
Look for signals like "approved", "on hold", "cancelled", "completed", etc.
|
||||||
|
|
||||||
|
## 7b: Open Item Resolution
|
||||||
|
|
||||||
|
Look for signals that tracked items are now complete.
|
||||||
|
|
||||||
|
## 7c: Role/Title Changes
|
||||||
|
|
||||||
|
Look for new titles in signatures or explicit announcements.
|
||||||
|
|
||||||
|
## 7d: Organization/Relationship Changes
|
||||||
|
|
||||||
|
Look for company changes, partnership announcements, etc.
|
||||||
|
|
||||||
|
## 7e: Build State Change List
|
||||||
|
|
||||||
|
Compile all detected state changes before writing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 8: Check for Duplicates and Conflicts
|
||||||
|
|
||||||
|
Before writing:
|
||||||
|
- Check if already processed this source
|
||||||
|
- Skip duplicate key facts
|
||||||
|
- Handle conflicting information by noting both versions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 9: Write Updates
|
||||||
|
|
||||||
|
## 9a: Create and Update Notes
|
||||||
|
|
||||||
|
**For new entities:**
|
||||||
|
```bash
|
||||||
|
executeCommand("write '{knowledge_folder}/People/Jennifer.md' '{content}'")
|
||||||
|
```
|
||||||
|
|
||||||
|
**For existing entities:**
|
||||||
|
- Read current content first
|
||||||
|
- Add activity entry at TOP (reverse chronological)
|
||||||
|
- Update "Last seen" date
|
||||||
|
- Add new key facts (skip duplicates)
|
||||||
|
- Add new open items
|
||||||
|
|
||||||
|
## 9b: Apply State Changes
|
||||||
|
|
||||||
|
Update all fields identified in Step 7.
|
||||||
|
|
||||||
|
## 9c: Update Aliases
|
||||||
|
|
||||||
|
Add newly discovered name variants to Aliases field.
|
||||||
|
|
||||||
|
## 9d: Writing Rules
|
||||||
|
|
||||||
|
- **Always use absolute paths** with format `[[Folder/Name]]` for all links
|
||||||
|
- Use YYYY-MM-DD format for dates
|
||||||
|
- Be concise: one line per activity entry
|
||||||
|
- Escape quotes properly in shell commands
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 10: Ensure Bidirectional Links
|
||||||
|
|
||||||
|
After writing, verify links go both ways.
|
||||||
|
|
||||||
|
## Absolute Link Format
|
||||||
|
|
||||||
|
**IMPORTANT:** Always use absolute links:
|
||||||
|
```markdown
|
||||||
|
[[People/Sarah Chen]]
|
||||||
|
[[Organizations/Acme Corp]]
|
||||||
|
[[Projects/Acme Integration]]
|
||||||
|
[[Topics/Security Compliance]]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bidirectional Link Rules
|
||||||
|
|
||||||
|
| If you add... | Then also add... |
|
||||||
|
|---------------|------------------|
|
||||||
|
| Person → Organization | Organization → Person |
|
||||||
|
| Person → Project | Project → Person |
|
||||||
|
| Project → Organization | Organization → Project |
|
||||||
|
| Project → Topic | Topic → Project |
|
||||||
|
| Person → Person | Person → Person (reverse) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Note Templates
|
||||||
|
|
||||||
|
## People
|
||||||
|
```markdown
|
||||||
|
# {Full Name}
|
||||||
|
|
||||||
|
## Info
|
||||||
|
**Role:** {role, inferred role, or Unknown}
|
||||||
|
**Organization:** [[Organizations/{organization}]] or leave blank
|
||||||
|
**Email:** {email or leave blank}
|
||||||
|
**Aliases:** {comma-separated: first name, nicknames, email}
|
||||||
|
**First met:** {YYYY-MM-DD}
|
||||||
|
**Last seen:** {YYYY-MM-DD}
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
{2-3 sentences: Who they are, why you know them.}
|
||||||
|
|
||||||
|
## Connected to
|
||||||
|
- [[Organizations/{Organization}]] — works at
|
||||||
|
- [[People/{Person}]] — {relationship}
|
||||||
|
- [[Projects/{Project}]] — {role}
|
||||||
|
|
||||||
|
## Activity
|
||||||
|
- **{YYYY-MM-DD}** ({meeting|email}): {Summary with [[Folder/Name]] links}
|
||||||
|
|
||||||
|
## Key facts
|
||||||
|
{Substantive facts only. Leave empty if none.}
|
||||||
|
|
||||||
|
## Open items
|
||||||
|
{Commitments and next steps only. Leave empty if none.}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Organizations
|
||||||
|
```markdown
|
||||||
|
# {Organization Name}
|
||||||
|
|
||||||
|
## Info
|
||||||
|
**Type:** {company|team|institution|other}
|
||||||
|
**Industry:** {industry or leave blank}
|
||||||
|
**Relationship:** {customer|prospect|partner|competitor|vendor|other}
|
||||||
|
**Domain:** {primary email domain}
|
||||||
|
**Aliases:** {short names, abbreviations}
|
||||||
|
**First met:** {YYYY-MM-DD}
|
||||||
|
**Last seen:** {YYYY-MM-DD}
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
{2-3 sentences: What this org is, what your relationship is.}
|
||||||
|
|
||||||
|
## People
|
||||||
|
- [[People/{Person}]] — {role}
|
||||||
|
|
||||||
|
## Contacts
|
||||||
|
{For contacts who have their own notes}
|
||||||
|
|
||||||
|
## Projects
|
||||||
|
- [[Projects/{Project}]] — {relationship}
|
||||||
|
|
||||||
|
## Activity
|
||||||
|
- **{YYYY-MM-DD}** ({meeting|email}): {Summary}
|
||||||
|
|
||||||
|
## Key facts
|
||||||
|
{Substantive facts only. Leave empty if none.}
|
||||||
|
|
||||||
|
## Open items
|
||||||
|
{Commitments and next steps only. Leave empty if none.}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Projects
|
||||||
|
```markdown
|
||||||
|
# {Project Name}
|
||||||
|
|
||||||
|
## Info
|
||||||
|
**Type:** {deal|product|initiative|hiring|other}
|
||||||
|
**Status:** {active|planning|on hold|completed|cancelled}
|
||||||
|
**Started:** {YYYY-MM-DD or leave blank}
|
||||||
|
**Last activity:** {YYYY-MM-DD}
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
{2-3 sentences: What this project is, goal, current state.}
|
||||||
|
|
||||||
|
## People
|
||||||
|
- [[People/{Person}]] — {role}
|
||||||
|
|
||||||
|
## Organizations
|
||||||
|
- [[Organizations/{Org}]] — {relationship}
|
||||||
|
|
||||||
|
## Related
|
||||||
|
- [[Topics/{Topic}]] — {relationship}
|
||||||
|
|
||||||
|
## Timeline
|
||||||
|
**{YYYY-MM-DD}** ({meeting|email})
|
||||||
|
{What happened.}
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
- **{YYYY-MM-DD}**: {Decision}
|
||||||
|
|
||||||
|
## Open items
|
||||||
|
{Commitments and next steps only.}
|
||||||
|
|
||||||
|
## Key facts
|
||||||
|
{Substantive facts only.}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Topics
|
||||||
|
```markdown
|
||||||
|
# {Topic Name}
|
||||||
|
|
||||||
|
## About
|
||||||
|
{1-2 sentences: What this topic covers.}
|
||||||
|
|
||||||
|
**Keywords:** {comma-separated}
|
||||||
|
**Aliases:** {other references}
|
||||||
|
**First mentioned:** {YYYY-MM-DD}
|
||||||
|
**Last mentioned:** {YYYY-MM-DD}
|
||||||
|
|
||||||
|
## Related
|
||||||
|
- [[People/{Person}]] — {relationship}
|
||||||
|
- [[Organizations/{Org}]] — {relationship}
|
||||||
|
- [[Projects/{Project}]] — {relationship}
|
||||||
|
|
||||||
|
## Log
|
||||||
|
**{YYYY-MM-DD}** ({meeting|email}: {title})
|
||||||
|
{Summary}
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
- **{YYYY-MM-DD}**: {Decision}
|
||||||
|
|
||||||
|
## Open items
|
||||||
|
{Commitments and next steps only.}
|
||||||
|
|
||||||
|
## Key facts
|
||||||
|
{Substantive facts only.}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Summary: Low Strictness Rules
|
||||||
|
|
||||||
|
| Source Type | Creates Notes? | Updates Notes? | Detects State Changes? |
|
||||||
|
|-------------|---------------|----------------|------------------------|
|
||||||
|
| Meeting | Yes — ALL external attendees | Yes | Yes |
|
||||||
|
| Email (any human sender) | Yes | Yes | Yes |
|
||||||
|
| Email (automated/newsletter) | No (SKIP) | No | No |
|
||||||
|
|
||||||
|
**Philosophy:** Capture broadly, filter later if needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Error Handling
|
||||||
|
|
||||||
|
1. **Missing data:** Leave blank or write "Unknown"
|
||||||
|
2. **Ambiguous names:** Create note with disambiguation note
|
||||||
|
3. **Conflicting info:** Note both versions
|
||||||
|
4. **grep returns nothing:** Create new notes
|
||||||
|
5. **State change unclear:** Log in activity but don't change the field
|
||||||
|
6. **Note file malformed:** Log warning, attempt partial update
|
||||||
|
7. **Shell command fails:** Log error, continue
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quality Checklist
|
||||||
|
|
||||||
|
Before completing, verify:
|
||||||
|
|
||||||
|
**Source Type:**
|
||||||
|
- [ ] Correctly identified as meeting or email
|
||||||
|
- [ ] Applied low strictness rules (capture broadly)
|
||||||
|
|
||||||
|
**Resolution:**
|
||||||
|
- [ ] Extracted all name variants
|
||||||
|
- [ ] Searched existing notes
|
||||||
|
- [ ] Built resolution map
|
||||||
|
- [ ] Used absolute paths `[[Folder/Name]]`
|
||||||
|
|
||||||
|
**Filtering:**
|
||||||
|
- [ ] Excluded only self and @user.domain
|
||||||
|
- [ ] Created notes for all external contacts
|
||||||
|
- [ ] Only skipped obvious automated/newsletters
|
||||||
|
|
||||||
|
**Content Quality:**
|
||||||
|
- [ ] Summaries describe relationship
|
||||||
|
- [ ] Roles inferred where possible
|
||||||
|
- [ ] Key facts are substantive
|
||||||
|
- [ ] Open items are commitments/next steps
|
||||||
|
|
||||||
|
**State Changes:**
|
||||||
|
- [ ] Detected and applied state changes
|
||||||
|
- [ ] Logged changes in activity
|
||||||
|
|
||||||
|
**Structure:**
|
||||||
|
- [ ] All links use `[[Folder/Name]]` format
|
||||||
|
- [ ] Activity entries reverse chronological
|
||||||
|
- [ ] Dates are YYYY-MM-DD
|
||||||
|
- [ ] Bidirectional links consistent
|
||||||
1054
apps/x/packages/core/src/knowledge/note_creation_medium.md
Normal file
1054
apps/x/packages/core/src/knowledge/note_creation_medium.md
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue