feat: persistent farm memory and enriched context bundle for AgriAgent

Farm memory (agrifine_farm_memory in chrome.storage.local):
- New FarmMemory schema: aiGeneratedSummary, farm_name, total_acres,
  primary_crops, soil_overview, key_insights, action_items, risk_flags,
  opportunities, lastUpdated
- getFarmMemory() / saveFarmMemory() in storage.js

buildContextBundle() now loads all four data sources in parallel:
  1. Farm memory snapshot (AI synthesis from prior sessions) — at top
  2. Field profiles with crop history, harvest records, and coordinates
  3. Ingested data files with structured-data previews
  4. Reading list articles with summaries and tags

Two new AgriAgent tools:
- get_farm_memory: retrieve the stored knowledge snapshot
- update_farm_memory: agent saves a comprehensive farm synthesis so future
  sessions start with full context (the key to persistent memory)

System prompt rewrite in agent.js:
- Agent now understands its role as the farm's persistent advisor
- Memory protocol: reference farm memory first, update it when new
  insights are discovered
- Explicit reasoning steps: Ground → Gaps → Connect → Cite → Remember
- Full tool selection guide with when-to-use guidance for all 11 tools

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBD2dN2KEjzz3UQFa9hEpu
This commit is contained in:
Claude 2026-06-27 06:12:05 +00:00
parent 7346642563
commit 05a3a3bbd0
No known key found for this signature in database
4 changed files with 204 additions and 31 deletions

View file

@ -26,16 +26,42 @@ export class AgrifineAgent {
const contextBundle = await buildContextBundle();
const systemPrompt = [
'You are AgriAgent, an expert AI assistant for farm operations management.',
'You have access to the user\'s farm data through tools — always use them before answering.',
'When answering questions about fields, weather, yields, or finances: first query the relevant data, then synthesize a clear answer.',
'Be specific: cite field names, dates, acreage, and numbers from the actual data.',
'For weather queries on a field, always look up the field profile first to get coordinates.',
'',
'FARM CONTEXT (reading list summaries + field profiles):',
contextBundle,
].join('\n');
const systemPrompt = `You are AgriAgent, the dedicated AI advisor for this farm operation. You maintain a persistent "farm memory" — a synthesized knowledge base you build and update over time as you learn more about the operation.
IDENTITY AND ROLE
- You are this farm's trusted advisor with deep, specific knowledge of its fields, crops, soils, finances, and operations.
- You think both tactically (today's weather, this week's harvest window) and strategically (long-term soil health, carbon sequestration, USDA program eligibility).
- Your answers are always grounded in the farm's actual data never guess or use generic advice when specific data is available.
MEMORY PROTOCOL
- The FARM CONTEXT section below is pre-loaded with all available data from every source, including your previously stored farm memory.
- The farm memory (if present) is the most important section it is your synthesized understanding of this operation built from prior analysis. Reference it first.
- When you discover something significant (a pattern, risk, or opportunity not already captured), call update_farm_memory to preserve it for future sessions.
- If farm memory is absent or stale (>14 days old), proactively synthesize one after reviewing the available field and file data.
REASONING APPROACH follow this order:
1. GROUND: What does the farm memory and pre-loaded context already tell me?
2. GAPS: What additional data do I need? Use the right tool don't query what's already in context.
3. CONNECT: Link data across sources (e.g. soil type + weather + crop history harvest recommendation).
4. CITE: Always name fields, dates, acreages, and numbers from the actual data.
5. REMEMBER: Did this conversation reveal anything new? If so, update_farm_memory.
TOOL SELECTION GUIDE
- get_field_profiles field locations, soil type, acreage, crop history, harvest records
- get_weather(lat, lon) live 7-day forecast + GDD; always get field coordinates first
- lookup_usda_soil(lat, lon) USDA soil classification and organic matter data
- get_ingested_files uploaded CSVs, Excel files, PDFs with extracted structured data
- get_reading_list saved articles, research, USDA notices
- calculate_gdd(highs, lows) growing degree day accumulation from temperature data
- screenshot_active_tab capture the current browser page as an image you can see
- get_page_content read text from the active tab or a saved reading-list URL
- open_tab(url) + read_tab_content(tab_id) navigate to a URL and parse its content
- export_farm_data generate and download CSV/JSON of farm data
- get_farm_memory retrieve the stored farm knowledge snapshot
- update_farm_memory save new insights about this farm for future sessions
FARM CONTEXT (all data sources pre-loaded memory, field profiles, ingested files, reading list):
${contextBundle}`;
const messages = [{ role: 'user', content: userMessage }];

View file

@ -12,14 +12,15 @@ const TOOL_ICONS = {
export_farm_data: '⬇️',
open_tab: '🌐',
read_tab_content: '📋',
get_farm_memory: '🧠',
update_farm_memory: '💾',
};
const SUGGESTED_PROMPTS = [
'Review all my farm data and build a farm memory summary',
'What are my current field conditions and harvest windows?',
'Which fields have the best soil for carbon sequestration?',
'Summarise all my farm data and flag any issues',
'What risks or opportunities do you see across my operation?',
'Screenshot this page and tell me what agricultural data you see',
'Read this page and save any farm data you find',
'Export my reading list and field profiles to CSV',
];

View file

@ -1,4 +1,4 @@
import { getReadingList, getIngestedFiles, getFieldProfiles } from '../utils/storage.js';
import { getReadingList, getIngestedFiles, getFieldProfiles, getFarmMemory, saveFarmMemory } from '../utils/storage.js';
function csvEscape(val) {
const s = String(val ?? '');
@ -140,6 +140,33 @@ export const TOOL_DEFINITIONS = [
required: ['data_type'],
},
},
{
name: 'get_farm_memory',
description: 'Retrieve the stored farm memory — the AI-synthesized knowledge base for this operation. Returns the most recent summary, key insights, action items, risk flags, and opportunities identified in prior sessions. Call this if the system context did not already include farm memory.',
input_schema: { type: 'object', properties: {}, required: [] },
},
{
name: 'update_farm_memory',
description: 'Save an updated farm memory snapshot. Call this after synthesizing new insights so future sessions benefit from what you learned. Write a comprehensive aiGeneratedSummary covering the whole farm operation — fields, soils, crops, patterns, and strategic outlook.',
input_schema: {
type: 'object',
properties: {
aiGeneratedSummary: {
type: 'string',
description: 'A rich narrative synthesis of the farm operation. Cover: total acreage, each field\'s status, soil conditions, crop history patterns, financial health (if data available), key risks, and strategic opportunities. Write as a briefing you\'d give a new advisor.',
},
farm_name: { type: 'string', description: 'Farm or operation name if known' },
total_acres: { type: 'number', description: 'Total acreage across all fields' },
primary_crops: { type: 'array', items: { type: 'string' }, description: 'Primary crops grown' },
soil_overview: { type: 'string', description: 'Summary of soil conditions across the operation' },
key_insights: { type: 'array', items: { type: 'string' }, description: 'Most important observations — patterns, correlations, or findings about this farm' },
action_items: { type: 'array', items: { type: 'string' }, description: 'Recommended next steps for the operator' },
risk_flags: { type: 'array', items: { type: 'string' }, description: 'Risks, concerns, or issues to monitor' },
opportunities: { type: 'array', items: { type: 'string' }, description: 'Opportunities identified — programs, practices, markets' },
},
required: ['aiGeneratedSummary'],
},
},
{
name: 'open_tab',
description: 'Open a URL in a new browser tab and wait for it to load. Use this to navigate to a relevant website — USDA, weather services, commodity markets, farm news, etc. After opening, call read_tab_content or screenshot_active_tab to extract information.',
@ -220,6 +247,10 @@ export async function executeTool(name, input) {
return toolGetPageContent(input);
case 'export_farm_data':
return toolExportFarmData(input);
case 'get_farm_memory':
return toolGetFarmMemory();
case 'update_farm_memory':
return toolUpdateFarmMemory(input);
case 'open_tab':
return toolOpenTab(input);
case 'read_tab_content':
@ -497,6 +528,32 @@ async function toolExportFarmData({ data_type, format = 'csv' }) {
return { exported: true, filename, record_count: records, format: filename.split('.').pop(), data_type };
}
async function toolGetFarmMemory() {
const memory = await getFarmMemory();
if (!memory) {
return {
has_memory: false,
message: 'No farm memory stored yet. Review the field profiles and ingested files, then call update_farm_memory to create a persistent knowledge base for this farm.',
};
}
return { has_memory: true, ...memory };
}
async function toolUpdateFarmMemory(input) {
await saveFarmMemory({
aiGeneratedSummary: input.aiGeneratedSummary,
farm_name: input.farm_name ?? null,
total_acres: input.total_acres ?? null,
primary_crops: input.primary_crops ?? [],
soil_overview: input.soil_overview ?? null,
key_insights: input.key_insights ?? [],
action_items: input.action_items ?? [],
risk_flags: input.risk_flags ?? [],
opportunities: input.opportunities ?? [],
});
return { saved: true, message: 'Farm memory updated. Future sessions will begin with this knowledge.' };
}
function toolOpenTab({ url, reason }) {
if (!url.startsWith('http://') && !url.startsWith('https://')) {
return Promise.resolve({ error: 'URL must start with http:// or https://' });

View file

@ -6,17 +6,33 @@
* agrifine_ingested_files Array<IngestedFile>
* agrifine_field_profiles Array<FieldProfile>
* agrifine_settings Settings
* agrifine_farm_memory FarmMemory (AI-synthesized knowledge base)
*
* chrome.storage.session keys:
* agrifine_api_key string (never persisted to local)
*
* FarmMemory shape:
* {
* lastUpdated: ISO string,
* aiGeneratedSummary: string, // Claude's narrative synthesis of the whole farm
* farm_name: string | null,
* total_acres: number | null,
* primary_crops: string[],
* soil_overview: string | null,
* key_insights: string[], // important observations
* action_items: string[], // recommended next steps
* risk_flags: string[], // risks or concerns to watch
* opportunities: string[], // identified opportunities
* }
*/
export const KEYS = {
READING_LIST: 'agrifine_reading_list',
INGESTED_FILES: 'agrifine_ingested_files',
FIELD_PROFILES: 'agrifine_field_profiles',
SETTINGS: 'agrifine_settings',
API_KEY: 'agrifine_api_key', // session only
READING_LIST: 'agrifine_reading_list',
INGESTED_FILES: 'agrifine_ingested_files',
FIELD_PROFILES: 'agrifine_field_profiles',
SETTINGS: 'agrifine_settings',
FARM_MEMORY: 'agrifine_farm_memory',
API_KEY: 'agrifine_api_key', // session only
};
// ── Generic helpers ──────────────────────────────────────────────────────────
@ -113,6 +129,16 @@ export async function deleteFieldProfile(id) {
await localSet(KEYS.FIELD_PROFILES, profiles.filter((p) => p.id !== id));
}
// ── Farm Memory ──────────────────────────────────────────────────────────────
export async function getFarmMemory() {
return (await localGet(KEYS.FARM_MEMORY)) ?? null;
}
export async function saveFarmMemory(memory) {
await localSet(KEYS.FARM_MEMORY, { ...memory, lastUpdated: new Date().toISOString() });
}
// ── Settings ─────────────────────────────────────────────────────────────────
export async function getSettings() {
@ -127,22 +153,85 @@ export async function saveSettings(patch) {
// ── Context bundle (used as AI system context) ───────────────────────────────
export async function buildContextBundle() {
const list = await getReadingList();
const profiles = await getFieldProfiles();
const [list, profiles, files, memory] = await Promise.all([
getReadingList(),
getFieldProfiles(),
getIngestedFiles(),
getFarmMemory(),
]);
const readingCtx = list.slice(0, 20).map((item) =>
`[${item.tags?.join(', ') ?? 'general'}] ${item.title}: ${item.summary ?? ''}`
).join('\n');
// ── 1. Farm memory (AI-synthesized knowledge — most important, goes first) ──
let memorySection;
if (memory) {
const lines = [
`FARM MEMORY (last updated ${memory.lastUpdated?.slice(0, 10) ?? 'unknown'}):`,
memory.aiGeneratedSummary ?? '',
];
if (memory.primary_crops?.length) lines.push(`Primary crops: ${memory.primary_crops.join(', ')}`);
if (memory.total_acres != null) lines.push(`Total acreage: ${memory.total_acres} ac`);
if (memory.key_insights?.length) {
lines.push('Key insights:');
memory.key_insights.forEach((s) => lines.push(`${s}`));
}
if (memory.action_items?.length) {
lines.push('Action items:');
memory.action_items.forEach((s) => lines.push(`${s}`));
}
if (memory.risk_flags?.length) {
lines.push('Risk flags:');
memory.risk_flags.forEach((s) => lines.push(`${s}`));
}
memorySection = lines.filter(Boolean).join('\n');
} else {
memorySection = 'FARM MEMORY: (none yet — after reviewing field data, call update_farm_memory to build a persistent knowledge base)';
}
const fieldCtx = profiles.map((p) =>
`Field "${p.name}" (${p.acres ?? '?'} ac, ${p.soilType ?? 'unknown soil'}): ${p.notes ?? ''}`
).join('\n');
// ── 2. Field profiles with crop history and harvest records ──────────────────
const fieldLines = profiles.length === 0 ? ['(none)'] : profiles.map((p) => {
const coords = p.coordinates?.lat != null && p.coordinates?.lon != null
? `${p.coordinates.lat.toFixed(4)}, ${p.coordinates.lon.toFixed(4)}`
: null;
const history = (p.cropHistory ?? []).slice(0, 4).map((h) => `${h.year}: ${h.crop}`).join(', ');
const harvests = (p.harvestRecords ?? []).slice(0, 3)
.map((h) => `${h.date?.slice(0, 10) ?? '?'}: ${h.yield} ${h.unit ?? ''}`.trim()).join('; ');
const parts = [
`Field "${p.name}" | ${p.acres ?? '?'} ac | ${p.soilType ?? 'unknown soil'}`,
coords ? ` Coords: ${coords}` : null,
p.cluId ? ` CLU: ${p.cluId}` : null,
history ? ` Crop history: ${history}` : null,
harvests ? ` Harvests: ${harvests}` : null,
p.notes ? ` Notes: ${p.notes}` : null,
];
return parts.filter(Boolean).join('\n');
});
// ── 3. Ingested data files ───────────────────────────────────────────────────
const fileLines = files.length === 0 ? ['(none)'] : files.slice(0, 10).map((f) => {
const preview = f.structuredData
? Object.entries(f.structuredData)
.filter(([k]) => k !== 'raw_preview' && k !== 'parse_error')
.slice(0, 5)
.map(([k, v]) => `${k}: ${JSON.stringify(v).slice(0, 120)}`)
.join(' | ')
: f.preview?.slice(0, 200) ?? '(no structured data)';
return `[${f.type}] ${f.filename} (${f.uploadedAt?.slice(0, 10) ?? '?'}): ${preview}`;
});
// ── 4. Reading list (recent saved articles) ──────────────────────────────────
const readingLines = list.length === 0 ? ['(none)'] : list.slice(0, 15).map((item) =>
`[${item.tags?.join(', ') ?? 'general'}] "${item.title}": ${item.summary ?? '(no summary)'}`
);
return [
'USER READING LIST CONTEXT:',
readingCtx || '(none)',
memorySection,
'',
'FIELD PROFILES:',
fieldCtx || '(none)',
'── FIELD PROFILES ──',
fieldLines.join('\n\n'),
'',
'── INGESTED DATA FILES ──',
fileLines.join('\n'),
'',
'── READING LIST (recent articles) ──',
readingLines.join('\n'),
].join('\n');
}