# ElizaOS Integration Guide
**For:** Registry Review MCP File Upload Feature
**Date:** 2025-11-17
**Updated:** 2025-11-17 (Added Phase 1: Deduplication)
**Status:** Ready for Integration
---
## Quick Start
The Registry Review MCP now supports file uploads! Your Eliza agent can accept PDF files from users and process them directly without filesystem access.
**New in Phase 1:** Automatic duplicate file detection and removal prevents accidental re-uploads and saves storage.
---
## Integration Example
### 1. Basic File Upload Handler
```typescript
import { Buffer } from 'buffer';
import { getLocalServerUrl } from '@elizaos/core';
async function handleRegistryReviewRequest(runtime, message) {
// Extract PDF attachments
const pdfAttachments = message.content.attachments.filter(
att => att.title?.endsWith('.pdf') || att.contentType === 'application/pdf'
);
if (pdfAttachments.length === 0) {
return "I don't see any PDF files attached. Please upload your project documents (PDFs) and I'll review them for registry compliance.";
}
try {
// Convert files to base64
const files = [];
for (const attachment of pdfAttachments) {
const response = await fetch(getLocalServerUrl(attachment.url));
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
files.push({
filename: attachment.title,
content_base64: buffer.toString('base64'),
mime_type: attachment.contentType || 'application/pdf'
});
}
// Call MCP tool for complete review
const result = await runtime.callMCPTool(
'registry-review',
'start_review_from_uploads',
{
project_name: extractProjectName(message) || `Review ${new Date().toISOString()}`,
files: files,
methodology: 'soil-carbon-v1.2.2',
auto_extract: true
}
);
return result;
} catch (error) {
return `Error processing documents: ${error.message}`;
}
}
function extractProjectName(message) {
// Extract project name from user message
// Example: "Review documents for Botany Farm 2022"
const match = message.content.text?.match(/for\s+([^.]+)/i);
return match ? match[1].trim() : null;
}
```
---
## Available MCP Tools
### 1. `start_review_from_uploads` ⭐ **Recommended**
**Best for:** Complete workflow in one step
```typescript
const result = await runtime.callMCPTool(
'registry-review',
'start_review_from_uploads',
{
project_name: "Botany Farm 2022",
files: [
{
filename: "ProjectPlan.pdf",
content_base64: base64EncodedContent,
mime_type: "application/pdf"
},
// ... more files
],
auto_extract: true // Get evidence coverage immediately
}
);
// Result includes:
// - Session creation details
// - Document classification summary
// - Evidence extraction results (if auto_extract=true)
// - Coverage statistics (23 requirements)
```
### 2. `create_session_from_uploads`
**Best for:** When you want to control evidence extraction timing
```typescript
const result = await runtime.callMCPTool(
'registry-review',
'create_session_from_uploads',
{
project_name: "My Project",
files: [...],
methodology: "soil-carbon-v1.2.2",
project_id: "C06-1234", // Optional
proponent: "Farm LLC", // Optional
crediting_period: "2022-2032" // Optional
}
);
// Later, extract evidence manually:
await runtime.callMCPTool('registry-review', 'extract_evidence', {
session_id: extractSessionId(result)
});
```
### 3. `upload_additional_files`
**Best for:** Adding files to an existing session
```typescript
// User uploads more files after initial review
const result = await runtime.callMCPTool(
'registry-review',
'upload_additional_files',
{
session_id: "session-abc123", // From previous create_session
files: [
{
filename: "AdditionalReport.pdf",
content_base64: newFileBase64
}
]
}
);
```
---
## Automatic Deduplication (Phase 1)
### What It Does
The upload tools now automatically detect and remove duplicate files:
- **Filename duplicates:** If the same filename appears multiple times, only the first is kept
- **Content duplicates:** If different filenames have identical content (SHA256), duplicates are removed
- **Transparent:** Happens automatically, no action required
- **Configurable:** Can be disabled with `deduplicate: false` if needed
### Default Behavior
Deduplication is **enabled by default** and works transparently:
```typescript
const result = await runtime.callMCPTool(
'registry-review',
'start_review_from_uploads',
{
project_name: "My Project",
files: [
{filename: "plan.pdf", content_base64: pdfData},
{filename: "plan.pdf", content_base64: pdfData}, // Duplicate - skipped
{filename: "plan-copy.pdf", content_base64: pdfData} // Same content - skipped
]
}
);
// Result includes deduplication info:
// - files_uploaded: 3
// - files_saved: 1
// - total_duplicates_removed: 2
```
### Disabling Deduplication
For special cases (testing, version tracking), you can disable it:
```typescript
const result = await runtime.callMCPTool(
'registry-review',
'create_session_from_uploads',
{
project_name: "Version Test",
files: [
{filename: "v1.pdf", content_base64: pdf},
{filename: "v2.pdf", content_base64: pdf} // Same content, both kept
],
deduplicate: false // Disable deduplication
}
);
```
### Benefits
- **Prevents user frustration:** No more accidental duplicate uploads
- **Saves storage:** Duplicate content is not stored multiple times
- **Faster processing:** Fewer files to process through document discovery
- **Clear feedback:** Response shows what was deduplicated and why
---
## Conversation Flow Examples
### Example 1: Simple Review
**User:** "Please review these project documents for registry compliance."
*[Uploads 3 PDFs]*
**Agent:**
```typescript
const result = await runtime.callMCPTool(
'registry-review',
'start_review_from_uploads',
{
project_name: "User Project Review",
files: convertAttachmentsToBase64(message.content.attachments),
auto_extract: true
}
);
```
**Response:** *(formatted from result)*
```
✓ Review Started from Uploads
Session ID: session-abc123
Files Uploaded (3):
- ProjectPlan.pdf
- BaselineReport.pdf
- MonitoringReport.pdf
Documents Found: 3
Evidence Extraction Complete:
Total Requirements: 23
✅ Covered: 15 (65.2%)
⚠️ Partial: 6 (26.1%)
❌ Missing: 2 (8.7%)
Overall Coverage: 78.3%
Next Steps:
- Review partial requirements for additional evidence
- Run cross-validation to verify date alignment
```
---
### Example 2: Iterative Upload
**User:** "I have the project plan ready. I'll send the baseline report later."
*[Uploads ProjectPlan.pdf]*
**Agent:**
```typescript
// First upload
const createResult = await runtime.callMCPTool(
'registry-review',
'create_session_from_uploads',
{
project_name: "Iterative Review",
files: [convertToBase64(projectPlan)],
auto_extract: false // Wait for all files
}
);
storeSessionId(extractSessionId(createResult)); // Save for later
```
**Response:**
```
✓ Session Created
Session ID: session-xyz789
Files Uploaded (1):
- ProjectPlan.pdf
I've started your review session. Upload the baseline report when ready, and I'll add it to the review.
```
**User:** *(later)* "Here's the baseline report."
*[Uploads BaselineReport.pdf]*
**Agent:**
```typescript
const addResult = await runtime.callMCPTool(
'registry-review',
'upload_additional_files',
{
session_id: retrieveSessionId(),
files: [convertToBase64(baselineReport)]
}
);
// Now extract evidence with all files
const evidenceResult = await runtime.callMCPTool(
'registry-review',
'extract_evidence',
{
session_id: retrieveSessionId()
}
);
```
**Response:**
```
✓ Files Added to Session
Added: BaselineReport.pdf
Total Documents: 2
Running evidence extraction with complete document set...
[Evidence results...]
```
---
## File Handling Best Practices
### 1. Validate File Types
```typescript
function validateAttachments(attachments) {
const validTypes = [
'application/pdf',
'application/x-pdf',
];
const pdfs = attachments.filter(att =>
validTypes.includes(att.contentType) ||
att.title?.toLowerCase().endsWith('.pdf')
);
if (pdfs.length === 0) {
throw new Error("No PDF files found. Please upload PDF documents.");
}
return pdfs;
}
```
### 2. Handle Large Files
```typescript
async function convertToBase64WithSizeCheck(attachment, maxSizeMB = 50) {
const response = await fetch(getLocalServerUrl(attachment.url));
const arrayBuffer = await response.arrayBuffer();
const sizeMB = arrayBuffer.byteLength / (1024 * 1024);
if (sizeMB > maxSizeMB) {
throw new Error(
`File ${attachment.title} is too large (${sizeMB.toFixed(1)}MB). ` +
`Maximum size is ${maxSizeMB}MB.`
);
}
const buffer = Buffer.from(arrayBuffer);
return buffer.toString('base64');
}
```
### 3. Provide Upload Feedback
```typescript
async function uploadWithProgress(runtime, files, projectName) {
// Send initial message
await sendMessage(`Uploading ${files.length} files...`);
// Convert files
const base64Files = [];
for (let i = 0; i < files.length; i++) {
await sendMessage(`Processing ${i + 1}/${files.length}: ${files[i].title}`);
const base64 = await convertToBase64(files[i]);
base64Files.push({
filename: files[i].title,
content_base64: base64
});
}
await sendMessage(`Starting registry review...`);
// Call MCP tool
const result = await runtime.callMCPTool(
'registry-review',
'start_review_from_uploads',
{
project_name: projectName,
files: base64Files,
auto_extract: true
}
);
return result;
}
```
---
## Error Handling
### Common Errors and Solutions
**1. Empty Files Array**
```
Error: At least one file is required
```
**Solution:** Validate attachments before calling tool
```typescript
if (pdfAttachments.length === 0) {
return "Please attach at least one PDF document.";
}
```
**2. Invalid Base64**
```
Error: Failed to decode base64 content for 'file.pdf'
```
**Solution:** Ensure proper buffer conversion
```typescript
const buffer = Buffer.from(await response.arrayBuffer());
const base64 = buffer.toString('base64'); // Not .toString() alone
```
**3. Missing Project Name**
```
Error: project_name is required and cannot be empty
```
**Solution:** Provide default or prompt user
```typescript
const projectName = extractFromMessage(message) ||
`Review ${new Date().toISOString().split('T')[0]}`;
```
**4. Duplicate Filename**
```
Error: File already exists in session directory: ProjectPlan.pdf
```
**Solution:** Inform user about duplicate
```typescript
try {
await runtime.callMCPTool('registry-review', 'upload_additional_files', {...});
} catch (error) {
if (error.message.includes('already exists')) {
return "That file is already in this session. Please use a different filename or upload a different file.";
}
throw error;
}
```
---
## Response Formatting
### Extract Key Information
```typescript
function formatReviewResults(mcpResult) {
// Parse the result text
const lines = mcpResult.split('\n');
// Extract session ID
const sessionLine = lines.find(l => l.includes('Session ID:'));
const sessionId = sessionLine?.split(':')[1]?.trim();
// Extract coverage stats
const coveredLine = lines.find(l => l.includes('✅ Covered:'));
const covered = coveredLine?.match(/\d+/)?.[0];
const partialLine = lines.find(l => l.includes('⚠️ Partial:'));
const partial = partialLine?.match(/\d+/)?.[0];
const missingLine = lines.find(l => l.includes('❌ Missing:'));
const missing = missingLine?.match(/\d+/)?.[0];
return {
sessionId,
coverage: {
covered: parseInt(covered || '0'),
partial: parseInt(partial || '0'),
missing: parseInt(missing || '0')
}
};
}
```
### Create User-Friendly Message
```typescript
function createUserMessage(reviewData) {
const { coverage } = reviewData;
const total = coverage.covered + coverage.partial + coverage.missing;
const percentage = ((coverage.covered + coverage.partial * 0.5) / total * 100).toFixed(1);
let message = `📊 Registry Review Complete!\n\n`;
message += `Coverage: ${percentage}% of requirements addressed\n\n`;
if (coverage.covered > 0) {
message += `✅ ${coverage.covered} requirements fully covered\n`;
}
if (coverage.partial > 0) {
message += `⚠️ ${coverage.partial} requirements partially covered (need more evidence)\n`;
}
if (coverage.missing > 0) {
message += `❌ ${coverage.missing} requirements missing\n`;
}
message += `\n`;
if (coverage.missing > 0) {
message += `⚠️ Action needed: ${coverage.missing} requirements are missing evidence. You may need to provide additional documents.\n`;
} else if (coverage.partial > 0) {
message += `💡 Tip: ${coverage.partial} requirements have partial evidence. Review them to see what's needed.\n`;
} else {
message += `🎉 Excellent! All requirements are addressed.\n`;
}
return message;
}
```
---
## Testing Your Integration
### 1. Test with Sample PDFs
```typescript
// Create minimal test PDF
function createTestPDF() {
const pdfContent = `%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000074 00000 n
0000000120 00000 n
trailer
<< /Size 4 /Root 1 0 R >>
startxref
149
%%EOF`;
return Buffer.from(pdfContent).toString('base64');
}
// Test the integration
async function testUpload() {
const result = await runtime.callMCPTool(
'registry-review',
'start_review_from_uploads',
{
project_name: "Test Project",
files: [
{
filename: "test.pdf",
content_base64: createTestPDF()
}
],
auto_extract: false // Skip evidence extraction for test
}
);
console.log("✓ Upload test passed:", result);
}
```
### 2. Test Error Handling
```typescript
async function testErrorHandling() {
// Test empty files
try {
await runtime.callMCPTool('registry-review', 'start_review_from_uploads', {
project_name: "Test",
files: []
});
console.error("❌ Should have thrown error for empty files");
} catch (error) {
console.log("✓ Empty files error handled correctly");
}
// Test invalid base64
try {
await runtime.callMCPTool('registry-review', 'start_review_from_uploads', {
project_name: "Test",
files: [{filename: "test.pdf", content_base64: "invalid!!!"}]
});
console.error("❌ Should have thrown error for invalid base64");
} catch (error) {
console.log("✓ Invalid base64 error handled correctly");
}
}
```
---
## Complete Integration Template
```typescript
import { Buffer } from 'buffer';
import { getLocalServerUrl } from '@elizaos/core';
// Main handler
export async function handleRegistryReview(runtime, message) {
try {
// 1. Validate attachments
const pdfAttachments = validatePDFAttachments(message.content.attachments);
// 2. Convert to base64
const files = await convertFilesToBase64(pdfAttachments);
// 3. Extract project name or use default
const projectName = extractProjectName(message) ||
`Review ${new Date().toISOString().split('T')[0]}`;
// 4. Call MCP tool
const result = await runtime.callMCPTool(
'registry-review',
'start_review_from_uploads',
{
project_name: projectName,
files: files,
methodology: 'soil-carbon-v1.2.2',
auto_extract: true
}
);
// 5. Format response for user
const reviewData = parseReviewResults(result);
return createUserMessage(reviewData);
} catch (error) {
return handleError(error);
}
}
// Helper functions
function validatePDFAttachments(attachments) {
const pdfs = attachments.filter(att =>
att.contentType === 'application/pdf' ||
att.title?.toLowerCase().endsWith('.pdf')
);
if (pdfs.length === 0) {
throw new Error("No PDF files found. Please upload PDF documents.");
}
return pdfs;
}
async function convertFilesToBase64(attachments) {
const files = [];
for (const att of attachments) {
const response = await fetch(getLocalServerUrl(att.url));
const buffer = Buffer.from(await response.arrayBuffer());
files.push({
filename: att.title,
content_base64: buffer.toString('base64'),
mime_type: att.contentType || 'application/pdf'
});
}
return files;
}
function extractProjectName(message) {
// Try to extract from message like "Review docs for Project X"
const text = message.content.text || '';
const match = text.match(/for\s+([^.]+)/i) ||
text.match(/project[:\s]+([^.]+)/i);
return match ? match[1].trim() : null;
}
function handleError(error) {
if (error.message.includes('At least one file is required')) {
return "Please attach at least one PDF document to review.";
}
if (error.message.includes('already exists')) {
return "One of the files is already in this review session. Please use a different filename.";
}
return `Error processing your request: ${error.message}`;
}
```
---
## Next Steps
1. **Copy the template** above into your ElizaOS action handler
2. **Test with sample PDFs** to verify the integration works
3. **Customize formatting** to match your agent's personality
4. **Add session management** if you want to support iterative uploads
5. **Deploy and monitor** for any edge cases
---
## Support
For issues or questions:
- Check `docs/FILE_UPLOAD_IMPLEMENTATION_2025-11-17.md` for detailed specs
- Check `docs/FILE_UPLOAD_ADDENDUM_PHASE1_2025-11-17.md` for deduplication details
- Review test examples in `tests/test_upload_tools.py`
- All 217 tests passing - the implementation is production-ready!
---
**Integration Guide Version:** 1.1
**Last Updated:** 2025-11-17
**Phase 1 (Deduplication):** Complete ✅
**Status:** Production Ready ✅