mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-26 00:46:23 +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
|
|
@ -2,6 +2,7 @@ import { jsonSchema, ModelMessage } from "ai";
|
|||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { WorkDir } from "../config/config.js";
|
||||
import { getNoteCreationStrictness } from "../config/note_creation_config.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 { 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;
|
||||
}
|
||||
|
||||
// Special case: load built-in agents from checked-in files
|
||||
// Built-in agents loaded from checked-in files
|
||||
const builtinAgents: Record<string, string> = {
|
||||
'note_creation': '../knowledge/note_creation.md',
|
||||
'meeting-prep': '../pre_built/meeting-prep.md',
|
||||
'email-draft': '../pre_built/email-draft.md',
|
||||
};
|
||||
|
||||
if (id in builtinAgents) {
|
||||
const currentDir = path.dirname(new URL(import.meta.url).pathname);
|
||||
const agentFilePath = path.join(currentDir, builtinAgents[id]);
|
||||
// Resolve agent file path (note_creation is dynamic based on strictness config)
|
||||
let agentFilePath: string | null = null;
|
||||
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");
|
||||
|
||||
let agent: z.infer<typeof Agent> = {
|
||||
|
|
|
|||
|
|
@ -13,4 +13,16 @@ function ensureDirs() {
|
|||
ensure(path.join(WorkDir, "knowledge"));
|
||||
}
|
||||
|
||||
ensureDirs();
|
||||
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();
|
||||
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`
|
||||
|
||||
## 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
|
||||
Change `BATCH_SIZE` in `build_graph.ts` (currently 25 files per batch)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { WorkDir } from '../config/config.js';
|
||||
import { autoConfigureStrictnessIfNeeded } from '../config/strictness_analyzer.js';
|
||||
import { createRun, createMessage } from '../runs/runs.js';
|
||||
import { bus } from '../runs/bus.js';
|
||||
import {
|
||||
|
|
@ -181,6 +182,9 @@ export async function buildGraph(sourceDir: string): Promise<void> {
|
|||
async function processAllSources(): Promise<void> {
|
||||
console.log('[GraphBuilder] Checking for new content in all sources...');
|
||||
|
||||
// Auto-configure strictness on first run if not already done
|
||||
autoConfigureStrictnessIfNeeded();
|
||||
|
||||
let anyFilesProcessed = false;
|
||||
|
||||
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