From f8234f5b3bd2f68dc6a0d26ff47d5a6a520b2964 Mon Sep 17 00:00:00 2001 From: Alex Co Date: Wed, 9 Oct 2024 14:51:55 +0800 Subject: [PATCH] Adding support for AT connector Signed-off-by: Alex Co --- backend/danswer/configs/constants.py | 1 + .../danswer/connectors/airtable/__init__.py | 0 .../danswer/connectors/airtable/connector.py | 70 ++++++++++++++ backend/danswer/connectors/factory.py | 5 +- backend/requirements/default.txt | 1 + web/public/Airtable.png | Bin 0 -> 11214 bytes web/src/components/icons/icons.tsx | 17 +++- web/src/lib/connectors/connectors.ts | 91 +++++++++++------- web/src/lib/connectors/credentials.ts | 8 ++ web/src/lib/sources.ts | 7 ++ web/src/lib/types.ts | 1 + 11 files changed, 163 insertions(+), 38 deletions(-) create mode 100644 backend/danswer/connectors/airtable/__init__.py create mode 100644 backend/danswer/connectors/airtable/connector.py create mode 100644 web/public/Airtable.png diff --git a/backend/danswer/configs/constants.py b/backend/danswer/configs/constants.py index 5ff2ccc7fd1..3e22e29df39 100644 --- a/backend/danswer/configs/constants.py +++ b/backend/danswer/configs/constants.py @@ -66,6 +66,7 @@ class DocumentSource(str, Enum): # Special case, document passed in via Danswer APIs without specifying a source type + AIRTABLE = "airtable" INGESTION_API = "ingestion_api" SLACK = "slack" WEB = "web" diff --git a/backend/danswer/connectors/airtable/__init__.py b/backend/danswer/connectors/airtable/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/backend/danswer/connectors/airtable/connector.py b/backend/danswer/connectors/airtable/connector.py new file mode 100644 index 00000000000..2cae2ca3a18 --- /dev/null +++ b/backend/danswer/connectors/airtable/connector.py @@ -0,0 +1,70 @@ +import json +from typing import Any + +from danswer.configs.app_configs import INDEX_BATCH_SIZE +from danswer.configs.constants import DocumentSource +from danswer.connectors.interfaces import GenerateDocumentsOutput +from danswer.connectors.interfaces import LoadConnector +from danswer.connectors.interfaces import PollConnector +from danswer.connectors.interfaces import SecondsSinceUnixEpoch +from danswer.connectors.models import Document +from danswer.connectors.models import Section +from pyairtable import Api as AirtableApi + + +class AirtableClientNotSetUpError(PermissionError): + def __init__(self) -> None: + super().__init__("Airtable Client is not set up, was load_credentials called?") + + +class AirtableConnector(LoadConnector, PollConnector): + def __init__( + self, + base_id: str, + table_name_or_id: str, + batch_size: int = INDEX_BATCH_SIZE, + ) -> None: + self.base_id = base_id + self.table_name_or_id = table_name_or_id + self.batch_size = batch_size + self.airtable_client: AirtableApi | None = None + + def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None: + self.airtable_client = AirtableApi(credentials["airtable_access_token"]) + + return None + + def poll_source( + self, start: SecondsSinceUnixEpoch | None, end: SecondsSinceUnixEpoch | None + ) -> GenerateDocumentsOutput: + if not self.airtable_client: + raise AirtableClientNotSetUpError() + + table = self.airtable_client.table(self.base_id, self.table_name_or_id) + all_records = table.all() + + record_documents = [] + for record in all_records: + record_document = Document( + id=str(record.get("id")), + sections=[ + Section( + link=f"https://airtable.com/{self.base_id}/{self.table_name_or_id}/", + text=json.dumps(record.get("fields")), + ) + ], + source=DocumentSource.AIRTABLE, + semantic_identifier=f"Airtable Base ID: {self.base_id}. Table Name or ID: {self.table_name_or_id}", + metadata={ + "type": "airtable", + "created_time": record.get("createdTime"), + }, + ) + record_documents.append(record_document) + + yield record_documents + + def load_from_state(self) -> GenerateDocumentsOutput: + if not self.airtable_client: + raise AirtableClientNotSetUpError() + return self.poll_source(None, None) diff --git a/backend/danswer/connectors/factory.py b/backend/danswer/connectors/factory.py index 1a3d605d3a5..8edfbc6a5d4 100644 --- a/backend/danswer/connectors/factory.py +++ b/backend/danswer/connectors/factory.py @@ -1,9 +1,8 @@ from typing import Any from typing import Type -from sqlalchemy.orm import Session - from danswer.configs.constants import DocumentSource +from danswer.connectors.airtable.connector import AirtableConnector from danswer.connectors.axero.connector import AxeroConnector from danswer.connectors.blob.connector import BlobStorageConnector from danswer.connectors.bookstack.connector import BookstackConnector @@ -45,6 +44,7 @@ from danswer.connectors.zulip.connector import ZulipConnector from danswer.db.credentials import backend_update_credential_json from danswer.db.models import Credential +from sqlalchemy.orm import Session class ConnectorMissingException(Exception): @@ -58,6 +58,7 @@ def identify_connector_class( connector_map = { DocumentSource.WEB: WebConnector, DocumentSource.FILE: LocalFileConnector, + DocumentSource.AIRTABLE: AirtableConnector, DocumentSource.SLACK: { InputType.LOAD_STATE: SlackLoadConnector, InputType.POLL: SlackPollConnector, diff --git a/backend/requirements/default.txt b/backend/requirements/default.txt index 5b9d57b9d35..fdc9f4768d4 100644 --- a/backend/requirements/default.txt +++ b/backend/requirements/default.txt @@ -74,3 +74,4 @@ zenpy==2.0.41 dropbox==11.36.2 boto3-stubs[s3]==1.34.133 ultimate_sitemap_parser==0.5 +pyairtable==3.0.0a3 \ No newline at end of file diff --git a/web/public/Airtable.png b/web/public/Airtable.png new file mode 100644 index 0000000000000000000000000000000000000000..7a25f56f9fc0f1ebd4107aa041e15e6712c6c560 GIT binary patch literal 11214 zcmeHscTiNz_U@2_WC@aV7(jv!aY#eXIU`Y!VHjWtGvp{aND?JUkemdGqU0P!l$?Wt zVjxJ)ao*@T_ndR<{it2^2b>5Kq+`ne*22w$Wf0N^`cocY7+e$%r^ z40|jI&K2NuXVsxl`p%DqhFHhMlx7RGTFf`MPz$AUomA>{)6jy zab;O&zdCpKf?P&6I5)R>sXB6Gq^0%~zfz9IV#!R8@zOTNSLWizvX^k9Nf1+M8xiUL z{x#{2x9A6ivDMFu7}u@>qN+NGGc|E`g_|=Suf9E( zL414L#y>Z|J=an_H@JKG?0|FN@a-mlu223$Keo%ppo`PNrnc?Ak&%%bgH9?B3aT;3 zTi&@MuVWMMRX=Up z8ty^IL>=oNPSIxF57eIKC~x zOIWxViHm!OIN&;ENzduMkTN!({haP5zF-}}v$HKI(ijmDf~Xk}N1nY}EX~(9cvqZa zUh!u9eQDWubH_?!g4muUgni%343P~{bH}Dsb#+YW% z^6ulq_mwlKmdk`&W&tk_7q)NNf?HqH=W4AzK4N#0ayk-JqROKE79XLUhQ}yY91@l= zn1N>dQTl<$z8x>POzE~+%4^mXL7{Zr=x!|I+sn-GV*Ci8jknAw?@3aqkV$MOtZH=_kMm=cezWw;_7oruaZE` ztmNaJGK5cprwI%Ek^A)|#3r-<+_~N@kBOz%grqK!^EXN;t>R`FV*I$ly z=j)JhGissex*Nvxccyn92_Anme)wYhtRQMWW3oze;%u1Br;}%sFl>7~!i+USBjAH* z>1!?UlsGrQM$7KWpp8&_!`7YFSNHkFMxN<7gGbtU3YyKfEYH-?`NQJ|AXeIsyDeGL z>jmG|$;v9%&!w|_X=joi6jrOmt`ob|WExLLG4PbvkiwOkfzOz{`PkC@9j5YHu6|e? zAI6{?sE59r=z_EEJOtu;BIiH&@K=EeH?N>!0sAs}`x0eZwQ_34I!+Z96w>);D>#>O z3{|GcwpujUvugsEc9#U+nB+tGnu}i6q|v+dgJfyjI9Ki3S$9py^lsV|xH)b$kC20y zf~qFXg$BwpP(!G+2pzjadp82-CJU7_*Q{*GE4!FU39lKS4MMV`Yw^<88oZ)^X!Q*F zI_4&~OhO%YV|DYLf*0~9PqL)92v#E=0v_R3ClZD@eV^DbQe&h(lpCt9;}PV2GEOLY zXNZ!b?po1gy_#>^+SZh&#)}c-tiGpcW)5+Vkhq-YJpJrP^FQxw_d7|>Na%3()5g-d zp}7nea=}(Et==qwgxtNg_ReDR0}LRberj{;qo5j7wHJKvrM%0645D{?uMsDkL8p@I zw#-?jAG}8-sZ#FUm6HTKf8;KuD@|9nor?nJmVq>j#c)2*yb}LRh!C;U{%~BuITjyF zB;m`6_ZFZnF(@o==-H^9!hCpB?tb&6b;gRr9yv-;FYtqMkZv4Nl>_%SZh1<&3mLAe zw)eWVyX+Y!M`Mb@>Li???iRD5rL(1;`8V=D%V8EU%zI?W19h}!y2m< z#W!vxenmz(2<64a;M^;8LxNLycN5;rH<;#SV^$wd(i~oE{p`_@?$(X4oaa!>oLXWl zJx%bWCMNGm|3PaC1FJ~T?=%m=HCc4OT&Ev6PK1$rB*7h(8H;+;c&EiqI9Vb@7nA`E*c2nHXmr%bZS zptL^I99TpH7=ZI1j~fhmZtswjiPhAy|aZ=x(7&{2?p!E+>&-+@4bP zl!qk)H%dLylCn?c{bLdKsQFeyBgXt!U0I`dhtjifQX`o9+d_?3ktCwHFX^q!7)~nE zV?|~RZ@H=`Nl8_*XU8lta${dh-Yq2}NBQ1y4V>`Wd29Fpsib5@o7#uS9I`6da2mrE zl%;D%uZ1$Ty(zlT0|#`S>PPkx_};8FM@=?478+4}x$B~z8Bu>4-UZYq!VmAdW~;t8 z@sd+)Fl4W%K5vWU+9jo3ZVEGSN>4hZcnow!-I*AIh~Qrx_B1l&$|u<@FdpqlTod*p zlT|2zld@DttBf|C-moDn#aAtpjL^z2O$^SHeO+|&K8o|pwf8m-xzL$FB*L1mDzs9L z8P8fZs>S$ynMN41yMu|*2J-RO^=$T8g8ONP313E46mK7{0(;tuu5I~-bj<#&3~}%6 zzpYFTsA)|Gx6vm?snOl}ivOteqzchVVy&z&b3R{Wour~lswWyl75{i+e$UTz+nL)M z(#7F6Ku&Z%PGSm~S|up1j#M5IhO{T#FG#s|HT2~BD@#wxFGSN+;r8Wl+h&T5mC@6P zHaE*m^k?;xQRS)!ID>sILmWw^CyTM|cJ(lomyGx&v-MoI+|4a4gSf@?AT^cKfS7To zN#gN>uDzI&7<{Wb9$oyG46Wnzmk=2G9d*&3ps@0Zhn_RwKJ73rA=v zuY4;F7)$~;eY`8b5@TZD7#ZsJe1<V4Ty7tehchS!?s=5aKbC{^cJA#HWsWa~feMnr_yb!ZG+%JT$RLfNfldnn_%^o3Bu97?;w)!rHCC#cG7zkJl1^V}Wg*vmE{!BEYr!I}1J7W zB4(_oBn8z#EP1O2nh0rGuXhgtdScGf=F)nV2?rAL&XynHvz$@(^5Kq*TW`cQ7Xi=l zW>buH=UjOk`5akzy31v4xafuU%7C=!`-M{q z?ulL^3-hXMSIcu5scK_3qc#yiN&ZYUQLQ))EW>)KSq7oq?gY!N(k9KhYZWyBe=u_q z4!i`GDQe|`H`^CU zFiS(q*GB^|ST!q^|F10O z9o-kw1yVTT@yF9(+`}m*iFj>`THFBk%VnIM`K}bRyl$iV5pqgMq~Yw=bQ()Gu?8Pa z(jZhSKlG^B=;`Bw0avaq$dV*)TU{pg^Hlj*e4Kto`)K*cA=B~$p|$!J63nxMsourw z9*eL>wubK}Lzkm!`*Xuo(ghMU#w8&45a!Kc>UK^oEypYB^=O03l(lDFZf^u|SvF!{ z5|vjYIa=?2q1qkGC#NOTpja)6XMPz`J7r9<9H_~a8ghqw{f6XfVWGvMx?Dn)1Zs2QMTA zoytkypd>8UI}q*dUst;tT@KPFExs#1R-Dl3q$eaU%;(){cr)`TjQfyRr8sF4TslB9 z&APhKwc^@4y>(K|8sE7fFQs3C%v%d3Ju$Mge>ZMTw0fpfA=e$wo#AAN5?9M&+?*|@ z@@$R&e!zfee3LBqUOp3$iBAHPA5mx+*ybZ)>ml^AF})e6#G%a`Y{UUpf4L7REmS?E zrlM^qlNHFH9L;}etcTzW?^;iBP{^Vf>~GVcj;YO|XN7uRxu4S@l8l0pLOiwlq&8g# zr2S}cUGgw);Is^|OU;N%UVeBQ=e9?fe+>SsJUEYdcKoty(vcGHW8788{h1xu)9d+L z^CE-kD};H2v1~F0^`kh@O^p7EOocu-`~CK8hk^J)?0c)mg>uq6N2N3y{Hv+n_q#-77iKr04Gn%+k7^d z{TVwkK@lEL)Z4Xg6IHpSr_ZA2ia>0i91?<9rb`tgW+0@)K(a|()VV}ph)t4C%x!D3 zm{Tp4H>%`RM0|6iS<|{w9M*MEYKLhM?pgnq!K>m6i{+QMUled~>qo%pN$mkBPHtuhqPHt7@fX zEl;VcP@9fOkNh-Jae{i(K5chfE2;<4d)7$iyp{YsvJ~*7F1QM$a{KCykJd+DGh+=o z2i{CJ;}(ub)$|!2mE*p+k!=>Ohl?sB?3S*_YEk*GK^yMfE}ta|9;(?lk|;iZtUP^V z=DN|8OP(sfrHZSIlR>oOcamsatAVg$#oO5d>mfVoqI6J>k=R|3Cyg`{kB6GZ5^6b7 zoyd7J*6GQOjdX*|`Rj`BQW`uHOFICZNt|oyVy0B}W?@1bl7xqZ22m4L&tv;ujHWem zr>`07cco7X1roBHyyaSfJ#=^rUPc+lKt(4>zK?~Hin`@butk`3s;i6z0V=Y|nnwd@WU4;2*(>JwL;5ki}W$y5R8Z zFw}4p$0L>#$0uzKAJ1ao3=Gyz9qL%fXv}B??=i*+u?JoDUOy6#nyF$O7Q3YHp1wvY+w(b3~o$zT|>j69;ljQ^V((9?8o4@2TL)Z0^w)QT=`3)b>ShsLir-{6nEUo6uFqq zh+rf#b)_pZ+22xDm~4t=s>s-_sNWOM-aAN*;cc5Q8p>br_97G+>m~2t($T&3z@SI2 zAdV$W5Py?77daouv@8cLSGApZ7F{|&pduIcoDmh@NZm{z zk>ul8a@I>6Dn}E_*-AmsFwJ;ayPP(76Gn5FRep^Bpj6QLIs74V=Pn*lomF z@f``u2$Lm@FDE~pchj;eE76e>UaLPMXyqRP)1`~VKP|jV^!d2eA6E|Cz?g$(_#jvs zm`S0{edjw7lps`os&Ji@$8OD*;5f9-T}y`xzam>NZyZGMqKB96R+?$!m$EWkxVA=^ zmA`iY@47XwyclaV;0~|blnAR7X(IV}i34%%<@$h$dqVNt6<_ey64%N0{>BwsSL1F| zW~9LKPN0k=$c}v!v=I8wS_R#0e zO{EU@vuxtzUe|X{)L6B|^9l>?9F4c!@K5;fo8KuL@(c<%w0!1RK^#YE(jRlMFYQCs zk`@--s*g`OyqT`>-j~y;Q|`u1^IFyl-LB-LgDmz{$yOrz_te+FetK}dtRqnxb2c3q z@cEUy)eno=)YJn`dMzRze2Wp4>S|6LBEQ*(?U#l8;a@*Hh)c1DinKm$$A5u#&ZzH} zm-8zA3Ejg*XRFMV9Vs#7mEN9b1VgRq6pacT+q_*DqDoMvloe0rTnUt!A7=ByKjMg6%Nv&Q_NTe$9tWioaX2`MzRws-v|Ht+>X6J?$1w!PDfhm3e9=$l zQ@a@O>RS;TCP)jHG8C97^Ro#xz5C|Xcz=b1V{dY={=Eqv+4No0Z>@c1cTCLob1oti zv)XGvcx5W8SyO911uDqXai+l7Rmyc@BWX9^243J*JpiRCRMFg!pw|l}oI4E1bZw(5 zFM1o(Xk+lQ`4jd9$s&A0RfgQ!pRm4g!;Q!Zz0>xsdNOoPciagK_QyX!;W5mBwde5o zd_u0c6h#pxq+?{?_dCxS0}bg{TL_wGT0do;iw6S6C-3bMii_i`kfyY4?Qc~beY%pK z__2V~=_Cy@)z$+lqTLZd2p@z`0Ickb^cDiikOHOLZS9~2N-BR+ zU`x^<2TxB|C_lfCj}M=ZFdy38o?lQxLV{mFh+jwuj75Mw{7{}SUogsp{TIa_97+fe zxI5C-6NyFve{sTW&|aR>AP}}4_^r!~uuufg$2TFt7~_VFO0M1Z)Lu z#f63J>_q=TrH=COgrVSwUsPCfJ|vdMP82(#FgrUi1R-Gqh6ss?f+d9DaImN-OaNjl zASNyW7y1WyfRRTlOuP_YEREwIi&72OdqPqe!c8tp6%`ZWUJFU!B0wXy5T7Ul_4f_Wmaqyj?1 zPysQhkeHDm1S%{Jg@}O#M4z0jn5TKn(o1V&Z=)#{Z|s{J*lszg(8$|Gz|$`VII;n!(!rQHM=m*lfuE zS33Mtv|owmfARWrGX58Pz*7IWlmAHH|H}1Wx&9*s{v+^zqwBwN{YMJ?N8tZP*Z()U zNdMkYAyC-YARp{@DVAhd0K3)N*V8gm79c}mFc`6+L(BwVU*mq4xC#byg}J=qeSd%% z1GM}+|8aV$H+r}yXSlCrdDhv783ABUmob+In2i$5+3Mlt)z;Y+W~Ujm-;bF$#!Nyn zXDgVKIn1;&W=0IN?1cGQj`H08_bkfcT{ zP-xKB<%@#hm#W&*)w7T3xj)|x4cxOnUip@`a@tIHXI0PoB(L;p?&C>S-5yx!0O@@c z7=8RWy}5@2j&&c$(?G`%AoqP1dlC)6vC&jj0z?zAs$mNxu4-l;0Km2Dziu2rX4VaC zC6TAPwldKVB4*m#0%VaQSOM`t>PqrP4xifQ=XCbuZV)b$fYl%Ik#*tYz}knzIxZ-&GY0crQT|}OxjjFk#*4cFu4I1!c2pbYA zXRM&eOy8TRkexASCsdrBdALxUsHT~o9?LX6@2_3QwBShTO56jtsJRzI=S!zkpe~8- zJ}g8zrqVxZOedfgOAyLiXNCoMn)$xOThvI5cg`lN7F_LZx&wH|r;QqC46bIFGR99N6N=BbC z(j{pUj`9zZ_PB*7Fy`DvG4@O_je{C<)v&rA_uRrnN#urP1Rf}&Ame&O*3;|q`o>#( zr(2gp2VvL`nek&l&WOk+#Ag~((-prD-&&flTs5tp7OG6LDvYJnPg)t)vk=PC|KL}$ zg~D`OwTbW93-op;)*>(9K^AA18x!&3Lp>i^sAe1`Z^@Dg+c!8fjckic6^Q*1uIiRR4?yCh_K@1B&VX*W8%sXGEW0ggf|t?hVudwvz~h|Mx>BN` zjNdBzoHJmPO1n+d>el#Zee#PX-3N=3Zlp}#4=7C@@E}R~gRLff;NLU5k58&oXEOSY zdyKb7&Tk+p-sL`M{~;o&ofwe2bTxx#m)Tu?3W!UMf^%aY5_P<_E%+>W!4u`@G^mg~ zliTOI?U3vwgkc`nc*&!odu16JJe93x=)b{jETnCGWj7`x6kS{?YLT+nfD6PUr=8h2 zoM8scIRGl;vSRZYcV)qGFG<2kYRB>Ac|)8bS{XLABjP=dCu6o&0QEarQI*BSa`c%~ z&v4R>@@^A*+;)i^-AU#suMdNWb`ih$+}gnmuq}YaNqozPIc5y|SlnmI0lx;w-Nu^y zGOVL!0uDhP#kZ05h?yNB(TS1@p-62D!hw?SNpC(3))ES@%U%#+#Pi!CCwxz^2?U_7 MtfN$=U>){<03It?I{*Lx literal 0 HcmV?d00001 diff --git a/web/src/components/icons/icons.tsx b/web/src/components/icons/icons.tsx index b5e735b0e65..53be424eec2 100644 --- a/web/src/components/icons/icons.tsx +++ b/web/src/components/icons/icons.tsx @@ -88,6 +88,7 @@ import voyageIcon from "../../../public/Voyage.png"; import googleIcon from "../../../public/Google.webp"; import { FaRobot } from "react-icons/fa"; +import airtableIcon from "../../../public/Airtable.png"; export interface IconProps { size?: number; @@ -999,6 +1000,20 @@ export const LightSettingsIcon = ({ ); }; +export const AirtableIcon = ({ + size = 16, + className = defaultTailwindCSS, +}: IconProps) => { + return ( +
+ Logo +
+ ); +}; + // // COMPANY LOGOS // @@ -2112,7 +2127,7 @@ export const CpuIcon = ({ diff --git a/web/src/lib/connectors/connectors.ts b/web/src/lib/connectors/connectors.ts index 7e56a498dcd..832c37d6c94 100644 --- a/web/src/lib/connectors/connectors.ts +++ b/web/src/lib/connectors/connectors.ts @@ -809,6 +809,28 @@ For example, specifying .*-support.* as a "channel" will cause the connector to }, ], }, + airtable: { + description: "Configure Airtable connector", + values: [ + { + type: "text", + query: "Enter the Airtable Base ID:", + label: "Base ID", + name: "base_id", + optional: false, + description: "The ID of the Airtable base you want to connect to.", + }, + { + type: "text", + query: "Enter the Airtable Table Name or ID:", + label: "Table Name or ID", + name: "table_name_or_id", + optional: false, + description: + "The name or ID of the specific table within the Airtable base.", + }, + ], + }, }; export function createConnectorInitialValues( connector: ConfigurableSources @@ -819,21 +841,18 @@ export function createConnectorInitialValues( name: "", groups: [], is_public: true, - ...configuration.values.reduce( - (acc, field) => { - if (field.type === "select") { - acc[field.name] = null; - } else if (field.type === "list") { - acc[field.name] = field.default || []; - } else if (field.type === "checkbox") { - acc[field.name] = field.default || false; - } else if (field.default !== undefined) { - acc[field.name] = field.default; - } - return acc; - }, - {} as { [record: string]: any } - ), + ...configuration.values.reduce((acc, field) => { + if (field.type === "select") { + acc[field.name] = null; + } else if (field.type === "list") { + acc[field.name] = field.default || []; + } else if (field.type === "checkbox") { + acc[field.name] = field.default || false; + } else if (field.default !== undefined) { + acc[field.name] = field.default; + } + return acc; + }, {} as { [record: string]: any }), }; } @@ -844,28 +863,25 @@ export function createConnectorValidationSchema( return Yup.object().shape({ name: Yup.string().required("Connector Name is required"), - ...configuration.values.reduce( - (acc, field) => { - let schema: any = - field.type === "select" - ? Yup.string() - : field.type === "list" - ? Yup.array().of(Yup.string()) - : field.type === "checkbox" - ? Yup.boolean() - : field.type === "file" - ? Yup.mixed() - : Yup.string(); + ...configuration.values.reduce((acc, field) => { + let schema: any = + field.type === "select" + ? Yup.string() + : field.type === "list" + ? Yup.array().of(Yup.string()) + : field.type === "checkbox" + ? Yup.boolean() + : field.type === "file" + ? Yup.mixed() + : Yup.string(); - if (!field.optional) { - schema = schema.required(`${field.label} is required`); - } + if (!field.optional) { + schema = schema.required(`${field.label} is required`); + } - acc[field.name] = schema; - return acc; - }, - {} as Record - ), + acc[field.name] = schema; + return acc; + }, {} as Record), // These are advanced settings indexingStart: Yup.string().nullable(), pruneFreq: Yup.number().min(0, "Prune frequency must be non-negative"), @@ -1060,3 +1076,8 @@ export interface MediaWikiConfig extends MediaWikiBaseConfig { } export interface WikipediaConfig extends MediaWikiBaseConfig {} + +export interface AirtableConfig { + base_id: string; + table_name_or_id: string; +} diff --git a/web/src/lib/connectors/credentials.ts b/web/src/lib/connectors/credentials.ts index 424a07c82fe..27d73a7b038 100644 --- a/web/src/lib/connectors/credentials.ts +++ b/web/src/lib/connectors/credentials.ts @@ -182,6 +182,10 @@ export interface AxeroCredentialJson { axero_api_token: string; } +export interface AirtableCredentialJson { + airtable_access_token: string; +} + export interface MediaWikiCredentialJson {} export interface WikipediaCredentialJson extends MediaWikiCredentialJson {} @@ -282,6 +286,7 @@ export const credentialTemplates: Record = { access_key_id: "", secret_access_key: "", } as OCICredentialJson, + airtable: { airtable_access_token: "" } as AirtableCredentialJson, google_sites: null, file: null, wikipedia: null, @@ -424,6 +429,9 @@ export const credentialDisplayNames: Record = { // Axero base_url: "Axero Base URL", axero_api_token: "Axero API Token", + + // Airtable + airtable_access_token: "Airtable Access Token", }; export function getDisplayNameForCredentialKey(key: string): string { return credentialDisplayNames[key] || key; diff --git a/web/src/lib/sources.ts b/web/src/lib/sources.ts index bbc63847adb..64ac2976163 100644 --- a/web/src/lib/sources.ts +++ b/web/src/lib/sources.ts @@ -36,6 +36,7 @@ import { OCIStorageIcon, GoogleStorageIcon, ColorSlackIcon, + AirtableIcon, } from "@/components/icons/icons"; import { ValidSources } from "./types"; import { @@ -277,6 +278,12 @@ const SOURCE_METADATA_MAP: SourceMap = { displayName: "Ingestion", category: SourceCategory.Other, }, + airtable: { + icon: AirtableIcon, + displayName: "Airtable", + category: SourceCategory.Wiki, + docs: "https://docs.danswer.dev/connectors/airtable", + }, // currently used for the Internet Search tool docs, which is why // a globe is used not_applicable: { diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 936e4e6c84c..312864f7320 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -212,6 +212,7 @@ export interface UserGroup { } const validSources = [ + "airtable", "web", "github", "gitlab",