Skip to content

Commit 2b055f3

Browse files
author
Michael Liebmann
committed
updated askCodaTable (removed oAI request, cleaning, added instruction param), updated Readme
1 parent 4c62b35 commit 2b055f3

File tree

2 files changed

+43
-114
lines changed

2 files changed

+43
-114
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
# Coda
22

3-
Connery plugin for Coda
3+
Connery plugin for Coda. It's primary purpose is to fetch content from Coda docs.
4+
5+
The plugin currently contains one action: `askCodaTable`:
6+
7+
- Allows you to ask questions about the content of a Coda table.
8+
- For optimal performance, the table should contain two columns named 'Question' and 'Answer'.
9+
- If the table structure and/or naming is different, the action will fetch up to the first 10 columns and use them as a fallback.
10+
- Allows you to provide additional instructions for the Connery assistant on how to handle the content.
11+
- This can be useful if the table contains additional information that is not part of the question and answer.
12+
- It can also be used to provide more context or output formatting instructions.
413

514
## Repository structure
615

src/actions/askCodaTable.ts

Lines changed: 33 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { ActionDefinition, ActionContext, OutputObject } from 'connery';
22
import axios from 'axios';
3-
import OpenAI from 'openai';
43

54
const actionDefinition: ActionDefinition = {
65
key: 'askCodaTable',
76
name: 'Ask Coda Table',
8-
description: 'This action enables users to ask questions and receive answers from a table in a Coda document with question and answer columns.',
7+
description: 'This action retrieves Q&A content from a table in a Coda document.',
98
type: 'read',
109
inputParameters: [
1110
{
@@ -27,30 +26,12 @@ const actionDefinition: ActionDefinition = {
2726
},
2827
},
2928
{
30-
key: 'openAiApiKey',
31-
name: 'OpenAI API Key',
32-
description: 'Your OpenAI API key without any restirctions',
29+
key: 'instructions',
30+
name: 'Instructions',
31+
description: 'Optional instructions for the content processing.',
3332
type: 'string',
3433
validation: {
35-
required: true,
36-
},
37-
},
38-
{
39-
key: 'openAiModel',
40-
name: 'OpenAI Model',
41-
description: 'The OpenAI model to use (e.g., gpt-4o-mini)',
42-
type: 'string',
43-
validation: {
44-
required: true,
45-
},
46-
},
47-
{
48-
key: 'userQuestion',
49-
name: 'User Question',
50-
description: 'The question to be answered based on the Coda Q&A table',
51-
type: 'string',
52-
validation: {
53-
required: true,
34+
required: false,
5435
},
5536
},
5637
],
@@ -59,9 +40,9 @@ const actionDefinition: ActionDefinition = {
5940
},
6041
outputParameters: [
6142
{
62-
key: 'textResponse',
63-
name: 'Text Response',
64-
description: 'The answer to the user question based on the Coda Q&A table',
43+
key: 'qaContent',
44+
name: 'Q&A Content',
45+
description: 'The Q&A content retrieved from the Coda table',
6546
type: 'string',
6647
validation: {
6748
required: true,
@@ -74,26 +55,17 @@ export default actionDefinition;
7455

7556
export async function handler({ input }: ActionContext): Promise<OutputObject> {
7657
try {
77-
// Extract Doc ID and Page Name from the provided Coda URL
7858
const { docId, pageName } = extractIdsFromUrl(input.codaUrl);
79-
80-
// Get the correct Page ID
8159
const pageId = await getPageId(docId, pageName, input.codaApiKey);
82-
83-
// Get page details
84-
const pageDetails = await getPageDetails(docId, pageId, input.codaApiKey);
85-
86-
// Fetch table IDs
8760
const tableIds = await fetchTableIds(docId, pageId, input.codaApiKey);
61+
let qaContent = await fetchQAContent(docId, tableIds, input.codaApiKey);
8862

89-
// Fetch Q&A content
90-
const qaContent = await fetchQAContent(docId, tableIds, input.codaApiKey);
91-
92-
// Get answer from OpenAI
93-
const answer = await getOpenAiAnswer(qaContent, input.userQuestion, input.openAiApiKey, input.openAiModel);
63+
if (input.instructions) {
64+
qaContent = `Instructions for the following content: ${input.instructions}\n\n${qaContent}`;
65+
}
9466

9567
return {
96-
textResponse: answer,
68+
qaContent: qaContent,
9769
};
9870
} catch (error) {
9971
const errorMessage = error instanceof Error ? error.message : String(error);
@@ -102,11 +74,8 @@ export async function handler({ input }: ActionContext): Promise<OutputObject> {
10274
}
10375

10476
function extractIdsFromUrl(url: string): { docId: string, pageName: string } {
105-
console.log('Extracting IDs from URL:', url);
10677
const urlObj = new URL(url);
107-
console.log('URL object:', urlObj);
10878
const pathParts = urlObj.pathname.split('/').filter(Boolean);
109-
console.log('Path parts:', pathParts);
11079

11180
if (pathParts.length < 2) {
11281
throw new Error('Invalid Coda URL format');
@@ -115,17 +84,12 @@ function extractIdsFromUrl(url: string): { docId: string, pageName: string } {
11584
let docId = '';
11685
let pageName = '';
11786

118-
console.log('First path part:', pathParts[0]);
119-
console.log('Second path part:', pathParts[1]);
120-
12187
if (pathParts[0] === 'd') {
12288
const docIdParts = pathParts[1].split('_');
12389
if (docIdParts.length > 1) {
12490
docId = docIdParts[docIdParts.length - 1]; // Get the last part after splitting by '_'
12591
docId = docId.startsWith('d') ? docId.slice(1) : docId; // Remove leading 'd' if present
12692
pageName = pathParts[2] || ''; // Page name is the third part, if present
127-
console.log('Extracted Doc ID:', docId);
128-
console.log('Extracted Page Name:', pageName);
12993
} else {
13094
throw new Error('Unable to extract Doc ID from the provided URL');
13195
}
@@ -176,7 +140,6 @@ async function fetchQAContent(docId: string, tableIds: string[], apiKey: string)
176140
let qaContent = '';
177141

178142
for (const tableId of tableIds) {
179-
180143
// Fetch column information
181144
const columnsUrl = `https://coda.io/apis/v1/docs/${docId}/tables/${tableId}/columns`;
182145
const columnsResponse = await axios.get(columnsUrl, {
@@ -193,76 +156,33 @@ async function fetchQAContent(docId: string, tableIds: string[], apiKey: string)
193156
const rows = rowsResponse.data.items;
194157

195158
if (rows.length > 0) {
196-
const columnNames = Object.keys(rows[0].values).map(id => columnMap.get(id) || id);
159+
const columnIds = Object.keys(rows[0].values);
160+
const questionColumn = columnIds.find(id => (columnMap.get(id) as string)?.toLowerCase().includes('question'));
161+
const answerColumn = columnIds.find(id => (columnMap.get(id) as string)?.toLowerCase().includes('answer'));
197162

198-
// Try to identify question and answer columns
199-
const questionColumn = Object.keys(rows[0].values).find(id => {
200-
const columnName = columnMap.get(id);
201-
return typeof columnName === 'string' &&
202-
(columnName.toLowerCase().includes('question'));
203-
});
204-
const answerColumn = Object.keys(rows[0].values).find(id => {
205-
const columnName = columnMap.get(id);
206-
return typeof columnName === 'string' &&
207-
(columnName.toLowerCase().includes('answer'));
208-
});
163+
const processRow = (values: string[]) => {
164+
if (values.every(v => typeof v === 'string')) {
165+
qaContent += values.map((v, i) => `${i === 0 ? 'Q' : 'A'}${i + 1}: ${v}`).join('\n') + '\n\n';
166+
}
167+
};
209168

210169
if (questionColumn && answerColumn) {
211-
for (const row of rows) {
212-
const question = row.values[questionColumn];
213-
const answer = row.values[answerColumn];
214-
if (typeof question === 'string' && typeof answer === 'string') {
215-
qaContent += `Q: ${question}\nA: ${answer}\n\n`;
216-
} else {
217-
console.log(`Skipped row: Invalid question or answer type`);
218-
}
219-
}
170+
rows.forEach((row: { values: Record<string, any> }) => processRow([row.values[questionColumn], row.values[answerColumn]]));
220171
} else {
221-
console.log('Could not identify question and answer columns. Using first two columns as fallback.');
222-
const columnIds = Object.keys(rows[0].values);
223-
for (const row of rows) {
224-
const question = row.values[columnIds[0]];
225-
const answer = row.values[columnIds[1]];
226-
if (typeof question === 'string' && typeof answer === 'string') {
227-
qaContent += `Q: ${question}\nA: ${answer}\n\n`;
228-
} else {
229-
console.log(`Skipped row: Invalid question or answer type`);
230-
}
231-
}
172+
// Fallback to using up to first ten columns
173+
rows.forEach((row: { values: Record<string, any> }) => {
174+
const availableColumnIds = columnIds.slice(0, 10);
175+
const values = availableColumnIds.map(id => row.values[id]);
176+
processRow(values);
177+
});
232178
}
233-
} else {
234-
console.log('No rows found in the table.');
179+
}
180+
181+
// Optionally, we can add a note about empty tables or skipped rows to the qaContent
182+
if (rows.length === 0) {
183+
qaContent += "Note: No rows found in this table.\n\n";
235184
}
236185
}
237186

238187
return qaContent.trim();
239188
}
240-
241-
async function getOpenAiAnswer(content: string, question: string, apiKey: string, model: string): Promise<string> {
242-
const openai = new OpenAI({ apiKey });
243-
244-
try {
245-
const completion = await openai.chat.completions.create({
246-
model: model,
247-
messages: [
248-
{ role: "system", content: "You are a helpful assistant that answers questions based strictly on the provided Q&A content source document. Only use the information explicitly provided in the document to answer questions. If the answer is not available in the content, respond with: 'I don't have enough information to answer that question'. Ensure you include all relevant details and nuances from the content. Do not omit important information, such as further details or links, which should be properly formatted in your response. If the content contains links, display them clearly in your answer.For longer responses, improve readability by organizing your answers into clear paragraphs."},
249-
{ role: "user", content: `Q&A Content:\n${content}\n\nQuestion: ${question}` },
250-
],
251-
});
252-
253-
return completion.choices[0]?.message?.content?.trim() ?? "No response generated";
254-
} catch (error) {
255-
const errorMessage = error instanceof Error ? error.message : String(error);
256-
throw new Error(`Failed to get OpenAI answer: ${errorMessage}`);
257-
}
258-
}
259-
260-
async function getPageDetails(docId: string, pageId: string, apiKey: string): Promise<any> {
261-
const url = `https://coda.io/apis/v1/docs/${docId}/pages/${pageId}`;
262-
263-
const response = await axios.get(url, {
264-
headers: { 'Authorization': `Bearer ${apiKey}` },
265-
});
266-
267-
return response.data;
268-
}

0 commit comments

Comments
 (0)