feat: Integration Hub for external tool connectivity
- Add native config option: `new Brainy({ integrations: true })`
- OData integration for Excel Power Query and Power BI
- Google Sheets integration with Apps Script
- Server-Sent Events (SSE) for real-time streaming
- Webhooks for push notifications
- Zero-config with sensible defaults
- Full tree-shaking when disabled
This commit is contained in:
parent
24039e8a1a
commit
b5bc9000cf
28 changed files with 8186 additions and 1 deletions
451
integrations/google-sheets/Code.gs
Normal file
451
integrations/google-sheets/Code.gs
Normal file
|
|
@ -0,0 +1,451 @@
|
|||
/**
|
||||
* Brainy Google Sheets Add-on
|
||||
*
|
||||
* Custom functions and sidebar for two-way sync with Brainy.
|
||||
*
|
||||
* Setup:
|
||||
* 1. Open Script Editor: Extensions → Apps Script
|
||||
* 2. Paste this code into Code.gs
|
||||
* 3. Set your Brainy URL: Tools → Script properties → Add BRAINY_URL
|
||||
* 4. Refresh your spreadsheet
|
||||
*
|
||||
* Custom Functions:
|
||||
* - =BRAINY_QUERY(query, limit) - Query entities
|
||||
* - =BRAINY_GET(id) - Get entity by ID
|
||||
* - =BRAINY_SIMILAR(text, limit) - Semantic search
|
||||
* - =BRAINY_RELATIONS(entityId, direction) - Get relationships
|
||||
* - =BRAINY_TYPES() - List available types
|
||||
*
|
||||
* For full documentation: https://github.com/soulcraft/brainy
|
||||
*/
|
||||
|
||||
// Configuration
|
||||
const CONFIG_BRAINY_URL = 'BRAINY_URL';
|
||||
const CONFIG_API_KEY = 'BRAINY_API_KEY';
|
||||
const DEFAULT_LIMIT = 100;
|
||||
|
||||
/**
|
||||
* Get Brainy URL from script properties
|
||||
*/
|
||||
function getBrainyUrl() {
|
||||
const props = PropertiesService.getScriptProperties();
|
||||
const url = props.getProperty(CONFIG_BRAINY_URL);
|
||||
if (!url) {
|
||||
throw new Error('Brainy URL not configured. Go to Extensions → Apps Script → Project Settings → Script Properties and add BRAINY_URL');
|
||||
}
|
||||
return url.replace(/\/$/, ''); // Remove trailing slash
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API key (optional)
|
||||
*/
|
||||
function getApiKey() {
|
||||
const props = PropertiesService.getScriptProperties();
|
||||
return props.getProperty(CONFIG_API_KEY) || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Make authenticated request to Brainy
|
||||
*/
|
||||
function brainyFetch(endpoint, options = {}) {
|
||||
const baseUrl = getBrainyUrl();
|
||||
const apiKey = getApiKey();
|
||||
|
||||
const url = `${baseUrl}/sheets${endpoint}`;
|
||||
|
||||
const fetchOptions = {
|
||||
method: options.method || 'GET',
|
||||
contentType: 'application/json',
|
||||
muteHttpExceptions: true,
|
||||
...options
|
||||
};
|
||||
|
||||
if (apiKey) {
|
||||
fetchOptions.headers = {
|
||||
...fetchOptions.headers,
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
};
|
||||
}
|
||||
|
||||
if (options.payload) {
|
||||
fetchOptions.payload = JSON.stringify(options.payload);
|
||||
}
|
||||
|
||||
const response = UrlFetchApp.fetch(url, fetchOptions);
|
||||
const code = response.getResponseCode();
|
||||
const text = response.getContentText();
|
||||
|
||||
if (code >= 400) {
|
||||
const error = JSON.parse(text);
|
||||
throw new Error(error.error || `HTTP ${code}`);
|
||||
}
|
||||
|
||||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
// ============= Custom Functions =============
|
||||
|
||||
/**
|
||||
* Query Brainy entities
|
||||
*
|
||||
* @param {string} query Semantic search query or type filter (e.g., "type:person")
|
||||
* @param {number} limit Maximum results (default: 100)
|
||||
* @return {Array} Results as rows with headers
|
||||
* @customfunction
|
||||
*/
|
||||
function BRAINY_QUERY(query, limit) {
|
||||
query = query || '';
|
||||
limit = limit || DEFAULT_LIMIT;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('limit', limit);
|
||||
|
||||
if (query) {
|
||||
// Check if it's a type filter
|
||||
if (query.toLowerCase().startsWith('type:')) {
|
||||
params.append('type', query.substring(5).trim());
|
||||
} else {
|
||||
params.append('q', query);
|
||||
}
|
||||
}
|
||||
|
||||
const result = brainyFetch(`/query?${params.toString()}`);
|
||||
|
||||
if (!result.rows || result.rows.length === 0) {
|
||||
return [['No results found']];
|
||||
}
|
||||
|
||||
return [result.headers, ...result.rows];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single entity by ID
|
||||
*
|
||||
* @param {string} id Entity ID
|
||||
* @return {Array} Entity as rows with headers
|
||||
* @customfunction
|
||||
*/
|
||||
function BRAINY_GET(id) {
|
||||
if (!id) {
|
||||
return [['Error: ID required']];
|
||||
}
|
||||
|
||||
const result = brainyFetch(`/entity/${id}`);
|
||||
|
||||
if (!result.rows || result.rows.length === 0) {
|
||||
return [['Entity not found']];
|
||||
}
|
||||
|
||||
return [result.headers, ...result.rows];
|
||||
}
|
||||
|
||||
/**
|
||||
* Semantic similarity search
|
||||
*
|
||||
* @param {string} text Text to find similar entities to
|
||||
* @param {number} limit Maximum results (default: 10)
|
||||
* @return {Array} Similar entities as rows with headers
|
||||
* @customfunction
|
||||
*/
|
||||
function BRAINY_SIMILAR(text, limit) {
|
||||
if (!text) {
|
||||
return [['Error: Search text required']];
|
||||
}
|
||||
|
||||
limit = limit || 10;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('q', text);
|
||||
params.append('limit', limit);
|
||||
|
||||
const result = brainyFetch(`/similar?${params.toString()}`);
|
||||
|
||||
if (!result.rows || result.rows.length === 0) {
|
||||
return [['No similar entities found']];
|
||||
}
|
||||
|
||||
return [result.headers, ...result.rows];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relationships for an entity
|
||||
*
|
||||
* @param {string} entityId Entity ID to get relationships for
|
||||
* @param {string} direction Direction: "from", "to", or "both" (default: "from")
|
||||
* @param {number} limit Maximum results (default: 100)
|
||||
* @return {Array} Relationships as rows with headers
|
||||
* @customfunction
|
||||
*/
|
||||
function BRAINY_RELATIONS(entityId, direction, limit) {
|
||||
if (!entityId) {
|
||||
return [['Error: Entity ID required']];
|
||||
}
|
||||
|
||||
direction = direction || 'from';
|
||||
limit = limit || DEFAULT_LIMIT;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('limit', limit);
|
||||
|
||||
if (direction === 'from' || direction === 'both') {
|
||||
params.append('from', entityId);
|
||||
}
|
||||
if (direction === 'to' || direction === 'both') {
|
||||
params.append('to', entityId);
|
||||
}
|
||||
|
||||
const result = brainyFetch(`/relations?${params.toString()}`);
|
||||
|
||||
if (!result.rows || result.rows.length === 0) {
|
||||
return [['No relationships found']];
|
||||
}
|
||||
|
||||
return [result.headers, ...result.rows];
|
||||
}
|
||||
|
||||
/**
|
||||
* List available entity types
|
||||
*
|
||||
* @return {Array} List of noun types
|
||||
* @customfunction
|
||||
*/
|
||||
function BRAINY_TYPES() {
|
||||
const result = brainyFetch('/schema');
|
||||
return [['Type'], ...result.nounTypes.map(t => [t])];
|
||||
}
|
||||
|
||||
/**
|
||||
* List available relationship types
|
||||
*
|
||||
* @return {Array} List of verb types
|
||||
* @customfunction
|
||||
*/
|
||||
function BRAINY_RELATION_TYPES() {
|
||||
const result = brainyFetch('/schema');
|
||||
return [['Type'], ...result.verbTypes.map(t => [t])];
|
||||
}
|
||||
|
||||
// ============= Menu & Sidebar =============
|
||||
|
||||
/**
|
||||
* Add menu on spreadsheet open
|
||||
*/
|
||||
function onOpen() {
|
||||
const ui = SpreadsheetApp.getUi();
|
||||
ui.createMenu('Brainy')
|
||||
.addItem('Open Sidebar', 'showSidebar')
|
||||
.addSeparator()
|
||||
.addItem('Configure Connection', 'showConfig')
|
||||
.addItem('Test Connection', 'testConnection')
|
||||
.addToUi();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the Brainy sidebar
|
||||
*/
|
||||
function showSidebar() {
|
||||
const html = HtmlService.createHtmlOutputFromFile('Sidebar')
|
||||
.setTitle('Brainy')
|
||||
.setWidth(300);
|
||||
SpreadsheetApp.getUi().showSidebar(html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show configuration dialog
|
||||
*/
|
||||
function showConfig() {
|
||||
const ui = SpreadsheetApp.getUi();
|
||||
const props = PropertiesService.getScriptProperties();
|
||||
const currentUrl = props.getProperty(CONFIG_BRAINY_URL) || '';
|
||||
|
||||
const response = ui.prompt(
|
||||
'Configure Brainy Connection',
|
||||
`Enter your Brainy server URL (current: ${currentUrl || 'not set'}):`,
|
||||
ui.ButtonSet.OK_CANCEL
|
||||
);
|
||||
|
||||
if (response.getSelectedButton() === ui.Button.OK) {
|
||||
const url = response.getResponseText().trim();
|
||||
if (url) {
|
||||
props.setProperty(CONFIG_BRAINY_URL, url);
|
||||
ui.alert('Configuration saved! URL: ' + url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the connection
|
||||
*/
|
||||
function testConnection() {
|
||||
const ui = SpreadsheetApp.getUi();
|
||||
try {
|
||||
const result = brainyFetch('/health');
|
||||
ui.alert('Connection successful!\n\nStatus: ' + result.status + '\nUptime: ' + Math.round(result.uptime / 1000) + 's');
|
||||
} catch (e) {
|
||||
ui.alert('Connection failed!\n\n' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ============= Sidebar Functions =============
|
||||
|
||||
/**
|
||||
* Get connection status for sidebar
|
||||
*/
|
||||
function getConnectionStatus() {
|
||||
try {
|
||||
const result = brainyFetch('/health');
|
||||
return {
|
||||
connected: true,
|
||||
url: getBrainyUrl(),
|
||||
status: result.status,
|
||||
uptime: result.uptime
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
connected: false,
|
||||
error: e.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query entities from sidebar
|
||||
*/
|
||||
function sidebarQuery(query, type, limit) {
|
||||
const params = new URLSearchParams();
|
||||
params.append('limit', limit || 50);
|
||||
|
||||
if (query) params.append('q', query);
|
||||
if (type) params.append('type', type);
|
||||
|
||||
return brainyFetch(`/query?${params.toString()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add entity from sidebar
|
||||
*/
|
||||
function sidebarAddEntity(type, data, metadata) {
|
||||
return brainyFetch('/add', {
|
||||
method: 'POST',
|
||||
payload: { type, data, metadata }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update entity from sidebar
|
||||
*/
|
||||
function sidebarUpdateEntity(id, data, metadata) {
|
||||
return brainyFetch('/update', {
|
||||
method: 'POST',
|
||||
payload: { id, data, metadata, merge: true }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete entity from sidebar
|
||||
*/
|
||||
function sidebarDeleteEntity(id) {
|
||||
return brainyFetch('/delete', {
|
||||
method: 'POST',
|
||||
payload: { id }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get schema for sidebar dropdowns
|
||||
*/
|
||||
function sidebarGetSchema() {
|
||||
return brainyFetch('/schema');
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert query result into sheet at selection
|
||||
*/
|
||||
function insertQueryResult(query, type, limit) {
|
||||
const sheet = SpreadsheetApp.getActiveSheet();
|
||||
const selection = sheet.getActiveCell();
|
||||
|
||||
const result = sidebarQuery(query, type, limit);
|
||||
|
||||
if (!result.rows || result.rows.length === 0) {
|
||||
selection.setValue('No results');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = [result.headers, ...result.rows];
|
||||
const range = sheet.getRange(
|
||||
selection.getRow(),
|
||||
selection.getColumn(),
|
||||
data.length,
|
||||
data[0].length
|
||||
);
|
||||
|
||||
range.setValues(data);
|
||||
|
||||
return {
|
||||
inserted: result.rows.length,
|
||||
columns: result.headers.length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync selected range to Brainy
|
||||
*/
|
||||
function syncRangeToBrainy(type) {
|
||||
const sheet = SpreadsheetApp.getActiveSheet();
|
||||
const range = sheet.getActiveRange();
|
||||
const values = range.getValues();
|
||||
|
||||
if (values.length < 2) {
|
||||
throw new Error('Need at least header row and one data row');
|
||||
}
|
||||
|
||||
const headers = values[0];
|
||||
const operations = [];
|
||||
|
||||
// Find ID column
|
||||
const idCol = headers.findIndex(h => h.toLowerCase() === 'id');
|
||||
|
||||
for (let i = 1; i < values.length; i++) {
|
||||
const row = values[i];
|
||||
const metadata = {};
|
||||
|
||||
for (let j = 0; j < headers.length; j++) {
|
||||
if (j !== idCol && row[j]) {
|
||||
metadata[headers[j]] = row[j];
|
||||
}
|
||||
}
|
||||
|
||||
if (idCol >= 0 && row[idCol]) {
|
||||
// Update existing
|
||||
operations.push({
|
||||
action: 'update',
|
||||
id: row[idCol],
|
||||
metadata
|
||||
});
|
||||
} else {
|
||||
// Add new
|
||||
operations.push({
|
||||
action: 'add',
|
||||
type: type,
|
||||
metadata
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const result = brainyFetch('/batch', {
|
||||
method: 'POST',
|
||||
payload: { operations }
|
||||
});
|
||||
|
||||
// Update IDs in sheet for new entities
|
||||
if (idCol >= 0) {
|
||||
for (let i = 0; i < result.results.length; i++) {
|
||||
if (result.results[i].action === 'add' && result.results[i].id) {
|
||||
sheet.getRange(range.getRow() + 1 + i, range.getColumn() + idCol).setValue(result.results[i].id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue