1
1
import { ActionDefinition , ActionContext , OutputObject } from 'connery' ;
2
2
import axios from 'axios' ;
3
- import OpenAI from 'openai' ;
4
3
5
4
const actionDefinition : ActionDefinition = {
6
5
key : 'askCodaTable' ,
7
6
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.' ,
9
8
type : 'read' ,
10
9
inputParameters : [
11
10
{
@@ -27,30 +26,12 @@ const actionDefinition: ActionDefinition = {
27
26
} ,
28
27
} ,
29
28
{
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. ' ,
33
32
type : 'string' ,
34
33
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 ,
54
35
} ,
55
36
} ,
56
37
] ,
@@ -59,9 +40,9 @@ const actionDefinition: ActionDefinition = {
59
40
} ,
60
41
outputParameters : [
61
42
{
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' ,
65
46
type : 'string' ,
66
47
validation : {
67
48
required : true ,
@@ -74,26 +55,17 @@ export default actionDefinition;
74
55
75
56
export async function handler ( { input } : ActionContext ) : Promise < OutputObject > {
76
57
try {
77
- // Extract Doc ID and Page Name from the provided Coda URL
78
58
const { docId, pageName } = extractIdsFromUrl ( input . codaUrl ) ;
79
-
80
- // Get the correct Page ID
81
59
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
87
60
const tableIds = await fetchTableIds ( docId , pageId , input . codaApiKey ) ;
61
+ let qaContent = await fetchQAContent ( docId , tableIds , input . codaApiKey ) ;
88
62
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
+ }
94
66
95
67
return {
96
- textResponse : answer ,
68
+ qaContent : qaContent ,
97
69
} ;
98
70
} catch ( error ) {
99
71
const errorMessage = error instanceof Error ? error . message : String ( error ) ;
@@ -102,11 +74,8 @@ export async function handler({ input }: ActionContext): Promise<OutputObject> {
102
74
}
103
75
104
76
function extractIdsFromUrl ( url : string ) : { docId : string , pageName : string } {
105
- console . log ( 'Extracting IDs from URL:' , url ) ;
106
77
const urlObj = new URL ( url ) ;
107
- console . log ( 'URL object:' , urlObj ) ;
108
78
const pathParts = urlObj . pathname . split ( '/' ) . filter ( Boolean ) ;
109
- console . log ( 'Path parts:' , pathParts ) ;
110
79
111
80
if ( pathParts . length < 2 ) {
112
81
throw new Error ( 'Invalid Coda URL format' ) ;
@@ -115,17 +84,12 @@ function extractIdsFromUrl(url: string): { docId: string, pageName: string } {
115
84
let docId = '' ;
116
85
let pageName = '' ;
117
86
118
- console . log ( 'First path part:' , pathParts [ 0 ] ) ;
119
- console . log ( 'Second path part:' , pathParts [ 1 ] ) ;
120
-
121
87
if ( pathParts [ 0 ] === 'd' ) {
122
88
const docIdParts = pathParts [ 1 ] . split ( '_' ) ;
123
89
if ( docIdParts . length > 1 ) {
124
90
docId = docIdParts [ docIdParts . length - 1 ] ; // Get the last part after splitting by '_'
125
91
docId = docId . startsWith ( 'd' ) ? docId . slice ( 1 ) : docId ; // Remove leading 'd' if present
126
92
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 ) ;
129
93
} else {
130
94
throw new Error ( 'Unable to extract Doc ID from the provided URL' ) ;
131
95
}
@@ -176,7 +140,6 @@ async function fetchQAContent(docId: string, tableIds: string[], apiKey: string)
176
140
let qaContent = '' ;
177
141
178
142
for ( const tableId of tableIds ) {
179
-
180
143
// Fetch column information
181
144
const columnsUrl = `https://coda.io/apis/v1/docs/${ docId } /tables/${ tableId } /columns` ;
182
145
const columnsResponse = await axios . get ( columnsUrl , {
@@ -193,76 +156,33 @@ async function fetchQAContent(docId: string, tableIds: string[], apiKey: string)
193
156
const rows = rowsResponse . data . items ;
194
157
195
158
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' ) ) ;
197
162
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
+ } ;
209
168
210
169
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 ] ] ) ) ;
220
171
} 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
+ } ) ;
232
178
}
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" ;
235
184
}
236
185
}
237
186
238
187
return qaContent . trim ( ) ;
239
188
}
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