├── LICENSE.md ├── README.md ├── config_file_setup ├── config_template.py └── example_company │ ├── example_company_background.md │ └── lease_ai_config.py └── workflow ├── .workflow.state ├── 0_workflow_config └── entry.py ├── 1_parse_thread └── entry.py ├── 2_semantic_routers └── entry.py ├── 3_reply_drafter_and_assembler └── entry.py ├── 4_sending_manager └── entry.py ├── 5_hubspot_crm_update └── entry.py ├── label_setup_script___disable_after_deployment └── entry.py └── workflow.yaml /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Autonomous Sales Inbox & CRM Assistant 2 | 3 | This app leverages generative AI to intelligently manage your sales inbox and CRM, auto-draft replies using your sales playbook & company branding, and more. 4 | 5 | 6 | ## Demo: 7 | 8 | https://www.loom.com/share/92dcb751ba6f4a16a54ffccf92a4178e?sid=88f35317-6cea-4a68-9582-49f254c3d860 9 | 10 | ## Features: 11 | 12 | **Auto-drafted Responses that Match How You Already Sell:** Generative AI systems deployed without relevant context are useless for problem solving. Inbox Assistant can be configured to match your existing sales process, drafting style, commonly used case studies, and even your scheduling links. By inserting your company branding, sales playbooks, case studies and other context inbox assistant will draft contextually-coherent, one-review-and-send replies; and is specifically excellent at answering those repeat questions and lead objections that your templates don't handle but are still the same question, over and over again. 13 | 14 | **Autonomous Message Classification & Inbox Management:** To successfully achieve the level of accuracy needed for auto-drafting, Inbox Assistant parses every new message that lands in your inbox and semantically classifies it, this has the added benefit of enabling a completely customizable inbox management and triage feature. Out of the inbox, it will filter high signal prospecting emails out from the noise of your inbox and surface them using labels such as: 15 | 16 | - ICP Type (custom to your company) 17 | - Deal stage (APPOINTMENT SCHEDULED, QUALIFIED TO BUY, etc.) 18 | - Sentiment (INTERESTED, NOT INTERESTED, LEAD, etc.) 19 | - ***Any Custom Label Group You Configure*** 20 | 21 | Once labeled, it easy to search and group by any label in your inbox, allowing for easy discovery and trend spotting, saving you time and hopefully easing the pain of context switching across deals. 22 | 23 | **Semantic CRM Automation:** Integrating with HubSpot, CRM assistant will create and update contacts, update and create deals, and automatically move deals through your funnel accordingly. This feature improves data quality inside HubSpot, as it only creates contacts for prospects, unlike the native HubSpot Gmail integration, which will create contacts for every single message that lands in your inbox. This feature is still in testing, and further functionality is planned. 24 | 25 | ## How it Works: 26 | 27 | **GPT-4-based Semantic Routing:** This app uses a series of layered semantic classifications to filter out spam, non-prospecting emails and other such noise so that only high signal emails are processed for auto-drafting. This is done by making use of GPT-4 function calling. It is extremely simple, making it deceivingly effective. [Diagram here](https://www.figma.com/file/VQslgQjpVLrJ8KNVrPl6vQ/Digital-Leverage-Inbox-Assistant-Semantic-Routing?type=whiteboard&node-id=0-1&t=IaGMYaFv8cMN3d04-0). 28 | 29 | **Full Thread Context + Natural Language Programming + Python:** Throughout the entire workflow, the full thread is processed to ensure accurate classification and contextually-correct responses, with GPT-4-turbo, the 128k context window is almost never capped out. The natural language instructions represented in prompts, in the semantic routers and drafting logic are integrated with regular python, enabling handling of nuanced, squishy problems like "how to respond to this email correctly" to be processed as structured data by a series of processor classes that orchestrate the Gmail and OpenAI APIs. 30 | 31 | **Easy Config File Customization:** This app makes use of a configuration file that can be easily changed to meet your specific company details, sales process and other nuances. In the config file you can update the organization information, the semantic routing logic, your drafting logic and more. 32 | 33 | ### How to Setup: 34 | 35 | Pipedream has workflow sharing functionality. You can clone a version of this app by clicking the link here. For those unfamiliar, Pipedream is a great workflow automation platform for devs that allows you to run code blocks sequentially and use pre-made triggers. In this case, Pipedream monitors the Gmail Inbox and triggers the workflow whenever a new message is received. 36 | 37 | If you are self-deploying and setting up this workflow, this does require some ability to understand basic technical concepts (API key, what a function does, json structure, etc.) but you're reading a README, so I am going to assume this understanding (if you don't have such understanding or would prefer a managed version, you can reach out to me [here](jacobdietle@generateleverage.com) to discuss implementation) 38 | 39 | **Step by Step Setup:** 40 | 41 | 1. **Clone Workflow to Pipedream using this [link](https://pipedream.com/new?h=tch_5ofXeg&via=digital-leverage).** 42 | 43 | 44 | 2. **Setup GCP Secrets according to this Pipedream [guide](https://pipedream.com/apps/gmail-custom-oauth).** 45 | 46 | 47 | 3. **Context sourcing: Find your brand book and other company context for use in config file** 48 | 49 | 4. **Config File Setup:** 50 | - **Mapping your company information into the config format:** 51 | - Review the Lease.ai config file for example of how to setup correctly 52 | - Use provided template to insert your company context into the template. Using an LLM to do this for you is a good idea ;) 53 | - **Label Setup:** 54 | - The first thing you will notice in the config file is a nested list of labels. Go to the last python step in the workflow called `label_setup_script_disable_after_setup`. 55 | - This script takes a list of labels of user set labels, creates each one in the Gmail API, and returns a nested list of labels by category with their corresponding Gmail API `label_id`. 56 | - This nested list corresponds to the semantic router layers, which you can customize as well (I do not recommend changing the `EmailClassification` and `EmailRelevancy` classes as their logic is generic) 57 | - Customize the label list to fit your sales process, run the script, and copy and paste the complete `label_name` and `label _id` list into the config. 58 | - **Set link injection and signature settings:** Any links you want the Inbox Assistant to use (case studies, scheduling links, etc.) are set here. You can also setup your signature as emails created via the API do not automatically use your signature set in the Gmail UI. 59 | - **Set Auto-reply or Auto-draft settings:** 0 for auto-draft and 1 for auto-reply. Use auto-reply at your own risk. 60 | - **Set Whitelist settings:** This allows you to safely test the auto-reply functionality with guardrails, when turned on, Inbox assistant will only be able to send replies to whitelisted domains, allowing you leave the auto-reply setting on without concern if set to only your company domain. 0 for off, 1 for on. 61 | 62 | 5. **Hubspot Private App Setup (Optional)** 63 | 64 | - You just need to create a Hubspot developer account, create a test app and configure the Pipedream Hubspot dev app accordingly (set scopes for app to allow all CRM permissions). Get started [here](https://developers.hubspot.com/get-started). 65 | 66 | 6. **Testing & Deployment** 67 | - Test the config file to make sure it runs and exports correctly. 68 | - We recommend starting with the auto-reply setting turned on with the whitelist setting active. This allows you to test a deployed version that will autoreply but is limited to autoreplying to approved email addresses. 69 | - Test each code step individually. (You will likely need to test the drafting step several times to find the drafting tone of voice and style that you want) 70 | - Deploy the workflow and begin sending test emails to the inbox to measure end to end functionality. 71 | - Swap the reply settings from auto-reply to auto-draft (or not, again up to you) and turn off the whitelist setting. You now have a Gen AI Sales Inbox & CRM Assistant. 72 | 73 | 74 | ### Feedback & Testing: 75 | 76 | **All feedback is welcome (especially constructive criticism)** - I am still exploring where to take this application. Feel free to open an issue in this repo. 77 | 78 | If you are interested in testing new features, I am open to granting more permissive licenses as a part of a product feedback and testing. 79 | 80 | For any feedback or inquiries, you can reach me at jacobdietle@generateleverage.com 81 | -------------------------------------------------------------------------------- /config_file_setup/config_template.py: -------------------------------------------------------------------------------- 1 | # Define your org's configuration and custom settings 2 | semantic_routers_config = { 3 | "org_name": "Lease.ai", 4 | # past in your own label list created by the label setup script here 5 | "email_labels": { 6 | "EmailClassification": {"NOT_FROM_REAL_PERSON": "Label_n"}, 7 | "EmailRelevancy": {"NON_PROSPECTING_RELATED": "Label_n"}, 8 | "EmailSentimentAndFunnelStage": { 9 | "LEAD": "Label_n", 10 | "INTERESTED": "Label_n", 11 | "QUALIFIED_TO_BUY": "Label_n", 12 | "APPOINTMENT_SCHEDULED": "Label_n", 13 | "PRESENTATION_SCHEDULED": "Label_n", 14 | "DECISION_MAKER_BOUGHT_IN": "Label_n", 15 | "CONTRACT_SENT": "Label_n", 16 | "CLOSED_WON": "Label_n" 17 | }, 18 | "EmailScenarioInquiryType": { 19 | "COLD_OUTBOUND_REPLY": "Label_n", 20 | "WARM_INTRO_REPLY": "Label_n", 21 | "ORGANIC_INBOUND": "Label_n" 22 | }, 23 | "EmailScenarioSenderCategory": { 24 | "ICP 1: PLACEHOLDER": "Label_n", 25 | "ICP 2: PLACEHOLDER": "Label_n", 26 | "ICP_OTHER": "Label_n" 27 | } 28 | }, 29 | "router_prompts": { 30 | "email_classification": """ 31 | Function: Email Classification 32 | Analyze the email and determine whether it requires a response or not. 33 | 34 | Ruleset: 35 | - Can either be 0 or 1. 36 | - 0 if the email is a notification, a billing reminder, a social media update, spam, cold sales email or some other sort of email that a person would not generally respond to. 37 | - 1 if the email is from a real person with a genuine inquiry that requires some sort of response. Specific notification exceptions for [example@example.com], as [reasoning]. 38 | """, 39 | "email_relevancy": """ 40 | Function: Email Relevancy Classification 41 | Analyze the email and determine whether it is relevant to the Organization inbox. 42 | 43 | Ruleset: 44 | - Can either be 0 or 1. 45 | - 0 if the email is not relevant to the Organization prospecting inbox. For example, if the email is part of an ongoing chain that the inbox was cc'd on but does not require a response or action, is an inbound cold sales email, related to a current ongoing project, or company operations. Even if the sender sends a personalized cold outreach message, they are not relevant to this outbound sales system. 46 | - 1 if the email is relevant to the Organization's sales and lead generation inbox. For example, if the email is a direct inquiry, a sales question, a lead trying to schedule a call, or requires some sort of action from the Organization inbox. 47 | """, 48 | "email_sensitivity": """ 49 | Function: Email Sensitivity Classification 50 | Analyze the email and determine whether it is sensitive and requires stakeholder attention. Can either be 0 or 1. 0 if the email is not sensitive, 1 if the email is sensitive and requires stakeholder attention. Sensitive emails include any regarding pricing, requests for a contract, contract negotiations, client complaints, project scoping, or basically any email where an SDR would pass off the email chain to legal or high executives. It does not pertain to questions around product offerings, or any questions that could be answered referencing a company brand book or plays playbook. 51 | 52 | Ruleset: 53 | - Can either be 0 or 1. 54 | - 0 if the email is not sensitive. 55 | - 1 if the email is sensitive and requires special treatment and/or stakeholder loop in. 56 | """, 57 | "email_sentiment_and_funnel_stage": """ 58 | Function: Sentiment and Funnel Stage Classification 59 | Determine the sentiment and stage of the sales funnel the email corresponds to. 60 | 61 | Ruleset: 62 | - Use the content of the email to identify keywords or phrases that indicate sentiment and the stage of the sales funnel. 63 | - Labels can include: {EmailSentimentAndFunnelStage} 64 | """, 65 | "email_scenario": """ 66 | Function: Email Scenario Identification 67 | Identify the type of inquiry, the ICP category of the sender, and any specific details related to the inquiry. 68 | 69 | Ruleset: 70 | - types of inquiry options: {EmailScenarioInquiryType} 71 | - sender category options: {EmailScenarioSenderCategory} 72 | """ 73 | }, 74 | "inquiry_types": "{EmailScenarioInquiryType}", 75 | "sender_categories": "{EmailScenarioSenderCategory}", 76 | 77 | "prompt_label_mapping": { 78 | "email_classification": ["EmailClassification"], 79 | "email_relevancy": ["EmailRelevancy"], 80 | "email_sentiment_and_funnel_stage": ["EmailSentimentAndFunnelStage"], 81 | "email_scenario": ["EmailScenarioInquiryType", "EmailScenarioSenderCategory"], 82 | "inquiry_types": ["EmailScenarioInquiryType"], 83 | "sender_categories": ["EmailScenarioSenderCategory"] 84 | # Add other mappings as necessary 85 | } 86 | } 87 | variable_context = """ 88 | 89 | You are tasked with drafting a contextually aware and scenario specific replies to emails. 90 | 91 | Company: Your Company Name Here 92 | 93 | One Liner: "Your Company One Liner Here" 94 | 95 | Make sure the email is personalized to the sender, addresses content relevantly and succinctly. Adopt the tone of voice and persona: 96 | 97 | Your tone of voice should be: [PLACEHOLDER] 98 | Your character should be: [PLACEHOLDER] 99 | Language: [PLACEHOLDER] 100 | Purpose: [PLACEHOLDER] 101 | 102 | Brand Proposition: 103 | 104 | Vision: [PLACEHOLDER] 105 | Mission: [PLACEHOLDER] 106 | What: [PLACEHOLDER] 107 | Value Props: [PLACEHOLDER] 108 | Values: [PLACEHOLDER] 109 | 110 | ## Solutions/Services Offered: 111 | 112 | - [PLACEHOLDER] 113 | 114 | - [PLACEHOLDER] 115 | 116 | - [PLACEHOLDER] 117 | 118 | ## Case Studies by Services/Solution 119 | 120 | ### Solution 1 121 | 122 | - **[CASE STUDY 1]** 123 | - **[CASE STUDY 2]** 124 | - **[CASE STUDY 3]** 125 | - Common Elements: [SOLUTION 1A , SOLUTION 1B, SOLUTION 1C] 126 | 127 | ### Solution 2 128 | 129 | - **[CASE STUDY 4]** 130 | - **[CASE STUDY 5]** 131 | - **[CASE STUDY 6]** 132 | - Common Elements: [SOLUTION 2A, SOLUTION 2B, SOLUTION 2C] 133 | 134 | 135 | ### Solution 3 136 | 137 | - **[CASE STUDY 7]** 138 | - **[CASE STUDY 8]** 139 | - **[CASE STUDY 9]** 140 | - Common Elements: [SOLUTION 3A, SOLUTION 3B, SOLUTION 3C] 141 | 142 | etc for as many solutions/services required. 143 | 144 | 145 | ### Exhaustive Case Study Details 146 | 147 | [CASE STUDY NAME 1]: [https://www.example.com/case-studies/case-study-1]: 148 | 149 | Solution: [PLACEHOLDER] 150 | Client/Customer: [PLACEHOLDER] 151 | Year: [PLACEHOLDER] 152 | 153 | ``` 154 | Solution 1, Solution 4, Solution 7 155 | ``` 156 | 157 | [CASE STUDY NAME 2]: [https://www.example.com/case-studies/case-study-2]: 158 | 159 | Solution: [PLACEHOLDER] 160 | Client/Customer: [PLACEHOLDER] 161 | Year: [PLACEHOLDER] 162 | 163 | ``` 164 | Solution 3, Solution 5, Solution 8 165 | ``` 166 | 167 | [CASE STUDY NAME 3]: [https://www.example.com/case-studies/case-study-3]: 168 | 169 | Solution: [PLACEHOLDER] 170 | Client/Customer: [PLACEHOLDER] 171 | Year: [PLACEHOLDER] 172 | 173 | ``` 174 | Solution 3, Solution 2, Solution 7 175 | ``` 176 | 177 | [CASE STUDY NAME 4]: [https://www.example.com/case-studies/case-study-4]: 178 | 179 | Solution: [PLACEHOLDER] 180 | Client/Customer: [PLACEHOLDER] 181 | Year: [PLACEHOLDER] 182 | 183 | ``` 184 | Solution 2, Solution 4, Solution 9 185 | ``` 186 | 187 | ## Ideal Customer Profiles 188 | 189 | 190 | ### Ideal Customer Profile (ICP) 1: [PLACEHOLDER] 191 | 192 | ONE LINER DESCRIPTON: "[PLACEHOLDER]" 193 | 194 | INDUSTRY: [PLACEHOLDER] 195 | 196 | GEOGRAPHY: [PLACEHOLDER] 197 | 198 | COMPANY SIZE: [PLACEHOLDER] 199 | 200 | BUDGET: [PLACEHOLDER] 201 | 202 | THEY ARE BUYING: 203 | 204 | - [SOLUTION 1] 205 | - [SOLUTION 2] 206 | - [SOLUTION 4] 207 | 208 | DECISION MAKERS: 209 | 210 | - CEO, CMO, CTO, CDO 211 | - Vice President 212 | - Head Of 213 | - Director 214 | 215 | PAIN POINTS: 216 | 217 | - [PLACEHOLDER] 218 | - [PLACEHOLDER] 219 | - [PLACEHOLDER] 220 | 221 | BUSINESS GOALS: 222 | 223 | - [PLACEHOLDER] 224 | - [PLACEHOLDER] 225 | - [PLACEHOLDER] 226 | 227 | TECHNOLOGIES: 228 | 229 | - [PLACEHOLDER] 230 | - [PLACEHOLDER] 231 | - [PLACEHOLDER] 232 | 233 | ATTRIBUTES: 234 | 235 | -- [PLACEHOLDER] 236 | - [PLACEHOLDER] 237 | - [PLACEHOLDER] 238 | 239 | **ICP 1: [PLACEHOLDER] MESSAGING** 240 | 241 | ### ICP 1 Main Message 242 | 243 | **who we are:** [PLACEHOLDER] 244 | 245 | **what we do:** [PLACEHOLDER] 246 | 247 | **how we do it:** 248 | 249 | - [PLACEHOLDER] 250 | - [PLACEHOLDER] 251 | - [PLACEHOLDER] 252 | 253 | Proof Point: 254 | 255 | **who we are:** [PLACEHOLDER] 256 | 257 | - [CASE STUDY 1] 258 | - [CASE STUDY 2] 259 | - [CASE STUDY 3] 260 | 261 | Someone we know: 262 | 263 | ``` 264 | 265 | [PLACEHOLDER EXAMPLE EMAIL] 266 | 267 | ``` 268 | 269 | Someone we don't know: 270 | 271 | ``` 272 | 273 | [PLACEHOLDER EXAMPLE EMAIL] 274 | 275 | ``` 276 | 277 | ### Ideal Customer Profile (ICP) 2: [PLACEHOLDER] 278 | 279 | ONE LINER DESCRIPTON: "[PLACEHOLDER]" 280 | 281 | INDUSTRY: [PLACEHOLDER] 282 | 283 | GEOGRAPHY: [PLACEHOLDER] 284 | 285 | COMPANY SIZE: [PLACEHOLDER] 286 | 287 | BUDGET: [PLACEHOLDER] 288 | 289 | THEY ARE BUYING: 290 | 291 | - [SOLUTION 1] 292 | - [SOLUTION 2] 293 | - [SOLUTION 4] 294 | 295 | DECISION MAKERS: 296 | 297 | - CEO, CMO, CTO, CDO 298 | - Vice President 299 | - Head Of 300 | - Director 301 | 302 | PAIN POINTS: 303 | 304 | - [PLACEHOLDER] 305 | - [PLACEHOLDER] 306 | - [PLACEHOLDER] 307 | 308 | BUSINESS GOALS: 309 | 310 | - [PLACEHOLDER] 311 | - [PLACEHOLDER] 312 | - [PLACEHOLDER] 313 | 314 | TECHNOLOGIES: 315 | 316 | - [PLACEHOLDER] 317 | - [PLACEHOLDER] 318 | - [PLACEHOLDER] 319 | 320 | ATTRIBUTES: 321 | 322 | -- [PLACEHOLDER] 323 | - [PLACEHOLDER] 324 | - [PLACEHOLDER] 325 | 326 | **ICP 2: [PLACEHOLDER] MESSAGING** 327 | 328 | ### ICP 2 Main Message 329 | 330 | **who we are:** [PLACEHOLDER] 331 | 332 | **what we do:** [PLACEHOLDER] 333 | 334 | **how we do it:** 335 | 336 | - [PLACEHOLDER] 337 | - [PLACEHOLDER] 338 | - [PLACEHOLDER] 339 | 340 | Proof Point: 341 | 342 | **who we are:** [PLACEHOLDER] 343 | 344 | - [CASE STUDY 1] 345 | - [CASE STUDY 2] 346 | - [CASE STUDY 3] 347 | 348 | Someone we know: 349 | 350 | ``` 351 | 352 | [PLACEHOLDER EXAMPLE EMAIL] 353 | 354 | ``` 355 | 356 | Someone we don't know: 357 | 358 | ``` 359 | 360 | [PLACEHOLDER EXAMPLE EMAIL] 361 | 362 | ``` 363 | 364 | ### Case Study ICP Pairs 365 | 366 | # Template for documenting how case studies align with Ideal Customer Profiles (ICPs) 367 | 368 | ## ICP 1: [Your ICP 1 Title Here] 369 | 370 | **[Case Study Name]:** 371 | **Summary:** Provide a brief overview of the project, highlighting key achievements and innovations. 372 | **Why They Fit:** Explain why this case study is relevant to ICP 1, focusing on the challenges addressed and the solutions provided. 373 | **How to Use in Selling:** Offer strategies on how to leverage this case study in discussions with potential clients fitting ICP 1. 374 | 375 | ## ICP 2: [Your ICP 2 Title Here] 376 | 377 | **[Case Study Name]:** 378 | **Summary:** A concise summary that captures the essence of the project, its goals, and outcomes. 379 | **Why They Fit:** Detail the connection between the case study and ICP 2, emphasizing the value delivered and problems solved. 380 | **How to Use in Selling:** Suggest ways this case study can be used to engage and convince prospects in ICP 2. 381 | 382 | # Add more ICP sections as needed, following the same structure. 383 | 384 | --- 385 | 386 | You should use the above context to ensure that all your replies are contextually relevant, accurately represent [ORGANIZATION] brand and offerings and represent the company in the highest light while personally addressing the sender’s message. 387 | 388 | When referencing a case study or other link, you should use the designated [PLACEHOLDER] signified by the [case study] or [here] for the hubspot scheduling link (exact match, always all proper capitalization case) placeholders. 389 | 390 | Offer the scheduling link when a lead inquries about learning more, requests a meeting, or otherwise makes sense. THE SCHEDULING LINK MUST BE INSERTED VIA [PLACEHOLDER] [here] - YOU MUST ALWAYS USE THIS [PLACEHOLDER]. 391 | 392 | You are to answer questions regarding examples of work, Organizaton's brand proposition, services provided and assist with scheduling. You are not authorized to make legally binding agreements, provide information on service costs, timelines or other sensitive information. Never answer contract questions, negotiate or otherwise provide sensitive information. 393 | 394 | Never bombard a recipient with too much context, use your best judgment to provide the minimum necessary context to get them excited about Organization without risking information overwhelm. 395 | 396 | DO NOT PRINT IN MARKDOWN FORMATTING, PRINT IN PLAIN TEXT. NEVER WRITE A SIGNATURE, IT WILL BE PRESET. DO NOT WRITE A SIGNATURE. 397 | """ 398 | 399 | # Assuming common_context and variable_context are defined above this snippet and contain the necessary text 400 | 401 | drafting_config = { 402 | "variable_context": variable_context, 403 | "signature_block": """ 404 | --- 405 | Your Name 406 | Your Position 407 | Your Company 408 | 409 | your-website.com 410 | """, 411 | "links_for_insertion": { 412 | # Define the placeholders and their corresponding links 413 | "[CASE STUDY NAME 1]": "https://www.example.com/case-studies/case-study-1", 414 | "[CASE STUDY NAME 2]": "https://www.example.com/case-studies/case-study-2", 415 | "[CASE STUDY NAME 3]": "https://www.example.com/case-studies/case-study-3", 416 | "[CASE STUDY NAME 4]": "https://www.example.com/case-studies/case-study-1", 417 | "[here]": "https://meetings.hubspot.com/firstname-lastname/test-demo-call" 418 | # ... add all necessary links here 419 | } 420 | } 421 | 422 | sending_manager_config = { 423 | "sending_email_address": "firstname@yourorg.com", 424 | "whitelisted_domains": { 425 | "enabled": 1, # 0 for off, 1 for on 426 | "domains": ["yourorg.com", "example.com"] 427 | }, 428 | "draft_or_autosend": 0 # 0 for draft, 1 for autosend 429 | } 430 | 431 | def handler(pd: "pipedream"): 432 | # Export the configuration for use in downstream steps 433 | print("Config Exported.") 434 | pd.export("semantic_routers_config", semantic_routers_config) 435 | pd.export("drafting_config", drafting_config) 436 | pd.export("sending_manager_config", sending_manager_config) 437 | #pd.export("hubspot_crm_config", hubspot_crm_config) 438 | 439 | -------------------------------------------------------------------------------- /config_file_setup/example_company/example_company_background.md: -------------------------------------------------------------------------------- 1 | # Lease.ai Company and Brand Profile 2 | 3 | ## Company Overview 4 | 5 | Company Name: Lease.ai 6 | 7 | - One Liner: "Revolutionizing leasing management with AI-driven efficiency and insights." 8 | 9 | Lease.ai is at the forefront of transforming the real estate leasing process through advanced artificial intelligence. Our mission is to empower real estate professionals by automating and optimizing leasing operations, ensuring data security, and providing actionable insights to enhance tenant satisfaction and maximize portfolio value. 10 | 11 | ### Brand Identity 12 | 13 | Tone of Voice: Professional, authoritative, innovative, approachable 14 | Character: Expert, reliable, forward-thinking, solution-oriented 15 | Language: Precise, engaging, informative, accessible 16 | Purpose: To streamline, optimize, and transform the leasing management process for businesses 17 | 18 | ### Brand Proposition 19 | 20 | Vision: To set a new standard in leasing management through AI innovation. 21 | Mission: To empower real estate professionals with intelligent leasing solutions that drive efficiency, enhance tenant satisfaction, and maximize portfolio value. 22 | Values: Innovation, Transparency, Reliability, Efficiency, Customer-Centricity 23 | 24 | ### Value Propositions: 25 | 26 | - Automated and optimized leasing processes 27 | - Actionable insights for decision-making 28 | - Reduced operational costs and improved efficiency 29 | - Enhanced tenant relations through personalized experiences 30 | 31 | ## Solutions 32 | 33 | ### 1. Leasing Process Automation Platform 34 | 35 | A comprehensive AI-driven platform that automates the entire leasing cycle, from listing to lease management, enhancing accuracy and efficiency. 36 | 37 | ### 2. Real Estate Privacy Data Solutions 38 | 39 | Advanced solutions focusing on the protection of sensitive data with encryption, access controls, and compliance with global data protection laws. 40 | 41 | ### 3. On-Prem Real Estate Gen AI Assistants for Large Firms 42 | 43 | Customizable, on-premise AI assistants 44 | 45 | ## Case Studies 46 | 47 | Commercial Real Estate Streamlining with Lease.ai: 48 | 49 | Solution Used: Leasing Process Automation Platform 50 | Client: Metro Commercial Properties 51 | Outcome: Achieved a 40% reduction in lease processing time and a 25% increase in tenant satisfaction through automated document handling and tenant screening. 52 | 53 | Data Security Overhaul for a Residential Complex: 54 | 55 | Solution Used: Real Estate Privacy Data Solutions 56 | Client: Urban Living Residential 57 | Outcome: Successfully mitigated data breach risks and achieved compliance with GDPR and CCPA, enhancing tenant trust and company reputation. 58 | 59 | Custom AI Assistant Deployment in a Global Real Estate Firm: 60 | 61 | Solution Used: On-Prem Real Estate Gen AI Assistants for Large Firms 62 | Client: Global Realty Inc. 63 | Outcome: Enabled the firm to manage its international portfolio more efficiently with personalized AI insights, leading to a 15% increase in operational efficiency and a 20% increase in annual revenue. 64 | 65 | Enhancing Property Management with AI: 66 | 67 | Solution Used: Leasing Process Automation Platform + On-Prem Real Estate Gen AI Assistants 68 | Client: Premier Property Management 69 | Outcome: Transformed property management operations by automating lease renewals and maintenance requests, resulting in a 50% decrease in operational costs and a 35% improvement in tenant retention rates. 70 | 71 | 72 | ## ICPS 73 | 74 | ICP 1: Large Commercial Real Estate Firms 75 | 76 | Industry Focus: Commercial Real Estate 77 | Geography: Globally, with a strong presence in urban markets 78 | Company Size: Large enterprises managing extensive property portfolios 79 | Budget: $500K+ 80 | Buying: Leasing Process Automation Platform, On-Prem Real Estate Gen AI Assistants 81 | Pain Points: Inefficient leasing processes, data privacy concerns, need for market insights 82 | Business Goals: Streamline operations, ensure data security, gain competitive market insights 83 | 84 | Messaging: "Transform your real estate operations with Lease.ai's cutting-edge AI solutions. Our platform not only streamlines your leasing processes but also ensures unparalleled data security and provides the insights you need to stay ahead in the market. Let's discuss how we can elevate your property portfolio to new heights." 85 | 86 | 87 | ICP 2: Property Management Companies 88 | 89 | Industry Focus: Residential and Commercial Property Management 90 | Geography: Nationwide, with properties in both urban and suburban areas 91 | Company Size: Medium to large, managing multiple properties 92 | Budget: $200K - $500K 93 | Buying: Real Estate Privacy Data Solutions, Leasing Process Automation Platform 94 | Pain Points: Data security and compliance, inefficient leasing and maintenance management 95 | Business Goals: Enhance operational efficiency, improve tenant satisfaction, secure tenant data 96 | 97 | Messaging: "Lease.ai is here to revolutionize the way you manage properties. From automating tedious leasing tasks to securing your tenants' data, our solutions are designed to improve operational efficiency and enhance tenant satisfaction. Discover the potential of AI in transforming your property management approach with Lease.ai." 98 | 99 | Lease.ai's comprehensive approach to leveraging AI technology addresses the unique challenges faced by the real estate industry, offering innovative solutions that drive progress and growth for our clients." 100 | 101 | -------------------------------------------------------------------------------- /config_file_setup/example_company/lease_ai_config.py: -------------------------------------------------------------------------------- 1 | # Define your org's configuration and custom settings 2 | semantic_routers_config = { 3 | "org_name": "Lease.ai", 4 | # past in your own label list created by the label setup script here 5 | "email_labels": { 6 | "EmailClassification": {"NOT_FROM_REAL_PERSON": "Label_7"}, 7 | "EmailRelevancy": {"NON_PROSPECTING_RELATED": "Label_8"}, 8 | "EmailSentimentAndFunnelStage": { 9 | "LEAD": "Label_9", 10 | "INTERESTED": "Label_10", 11 | "QUALIFIED_TO_BUY": "Label_11", 12 | "APPOINTMENT_SCHEDULED": "Label_12", 13 | "PRESENTATION_SCHEDULED": "Label_13", 14 | "DECISION_MAKER_BOUGHT_IN": "Label_14", 15 | "CONTRACT_SENT": "Label_15", 16 | "CLOSED_WON": "Label_16" 17 | }, 18 | "EmailScenarioInquiryType": { 19 | "COLD_OUTBOUND_REPLY": "Label_17", 20 | "WARM_INTRO_REPLY": "Label_18", 21 | "ORGANIC_INBOUND": "Label_19" 22 | }, 23 | "EmailScenarioSenderCategory": { 24 | "ICP 1: Large Commercial Real Estate Firms": "Label_20", 25 | "ICP 2: Property Management Companies": "Label_21", 26 | "ICP_OTHER": "Label_22" 27 | } 28 | }, 29 | "router_prompts": { 30 | "email_classification": """ 31 | Function: Email Classification 32 | Analyze the email and determine whether it requires a response or not. 33 | 34 | Ruleset: 35 | - Can either be 0 or 1. 36 | - 0 if the email is a notification, a billing reminder, a social media update, spam, cold sales email or some other sort of email that a person would not generally respond to. 37 | - 1 if the email is from a real person with a genuine inquiry that requires some sort of response. Specific notification exceptions for [example@example.com], as [reasoning]. 38 | """, 39 | "email_relevancy": """ 40 | Function: Email Relevancy Classification 41 | Analyze the email and determine whether it is relevant to the Organization inbox. 42 | 43 | Ruleset: 44 | - Can either be 0 or 1. 45 | - 0 if the email is not relevant to the Organization prospecting inbox. For example, if the email is part of an ongoing chain that the inbox was cc'd on but does not require a response or action, is an inbound cold sales email, related to a current ongoing project, or company operations. Even if the sender sends a personalized cold outreach message, they are not relevant to this outbound sales system. 46 | - 1 if the email is relevant to the Organization's sales and lead generation inbox. For example, if the email is a direct inquiry, a sales question, a lead trying to schedule a call, or requires some sort of action from the Organization inbox. 47 | """, 48 | "email_sensitivity": """ 49 | Function: Email Sensitivity Classification 50 | Analyze the email and determine whether it is sensitive and requires stakeholder attention. Can either be 0 or 1. 0 if the email is not sensitive, 1 if the email is sensitive and requires stakeholder attention. Sensitive emails include any regarding pricing, requests for a contract, contract negotiations, client complaints, project scoping, or basically any email where an SDR would pass off the email chain to legal or high executives. It does not pertain to questions around product offerings, or any questions that could be answered referencing a company brand book or plays playbook. 51 | 52 | Ruleset: 53 | - Can either be 0 or 1. 54 | - 0 if the email is not sensitive. 55 | - 1 if the email is sensitive and requires special treatment and/or stakeholder loop in. 56 | """, 57 | "email_sentiment_and_funnel_stage": """ 58 | Function: Sentiment and Funnel Stage Classification 59 | Determine the sentiment and stage of the sales funnel the email corresponds to. 60 | 61 | Ruleset: 62 | - Use the content of the email to identify keywords or phrases that indicate sentiment and the stage of the sales funnel. 63 | - Labels can include: {EmailSentimentAndFunnelStage} 64 | """, 65 | "email_scenario": """ 66 | Function: Email Scenario Identification 67 | Identify the type of inquiry, the ICP category of the sender, and any specific details related to the inquiry. 68 | 69 | Ruleset: 70 | - types of inquiry options: {EmailScenarioInquiryType} 71 | - sender category options: {EmailScenarioSenderCategory} 72 | """ 73 | }, 74 | "inquiry_types": "{EmailScenarioInquiryType}", 75 | "sender_categories": "{EmailScenarioSenderCategory}", 76 | 77 | "prompt_label_mapping": { 78 | "email_classification": ["EmailClassification"], 79 | "email_relevancy": ["EmailRelevancy"], 80 | "email_sentiment_and_funnel_stage": ["EmailSentimentAndFunnelStage"], 81 | "email_scenario": ["EmailScenarioInquiryType", "EmailScenarioSenderCategory"], 82 | "inquiry_types": ["EmailScenarioInquiryType"], 83 | "sender_categories": ["EmailScenarioSenderCategory"] 84 | # Add other mappings as necessary 85 | } 86 | } 87 | 88 | 89 | common_context = """ 90 | You are monitoring an Organization's primary sales, lead generation and general inbound email account. You have one task as a part of a larger system 91 | which will be described below. For context the larger system designed to provide an automated, intelligent solution for managing emails at the Organization. It is comprised of: 92 | 1. Email Classification (is this email from a real person or a notification/spam?) 93 | 2. Email Scenario Identification (what is the nature of the email? Who is it from? What is it about?) 94 | 3. Funnel Stage Classification (what stage of the sales funnel does this email seem like?) 95 | 4. Sensitivity Determination and Stakeholder Notification (if the email is from a real person, should a stakeholder handle it?) 96 | 97 | Do your best to address the task below in the context of the larger system. Prioritize accuracy, and think quietly to yourself before responding. 98 | 99 | YOUR ACTIONS MUST STRICTLY FOLLOW THE PROVIDED RULESETS IN EACH FUNCTION. IF YOU GENERATE ANYTHING OUTSIDE OF THE RULESET, YOU FAIL THE TEST. 100 | 101 | You are responsible for the below task: 102 | """ 103 | 104 | class PromptAssembler: 105 | def __init__(self, config): 106 | self.config = config 107 | 108 | def assemble_prompt(self, prompt_key, **kwargs): 109 | template = self.config["router_prompts"][prompt_key] 110 | label_categories = self.config["prompt_label_mapping"].get(prompt_key, []) 111 | 112 | for category in label_categories: 113 | if category in self.config["email_labels"]: 114 | # Concatenate all label names (keys) into a single string 115 | labels_string = ", ".join(self.config["email_labels"][category].keys()) 116 | # Replace the placeholder with this string of names 117 | template = template.replace(f"{{{category}}}", labels_string) 118 | elif category == "inquiry_types" or category == "sender_categories": 119 | # Directly use the string for inquiry_types and sender_categories without looking up labels 120 | template = template.replace(f"{{{category}}}", self.config[category]) 121 | 122 | # Combine each prompt with the common_context 123 | combined_template = common_context + template 124 | return combined_template 125 | 126 | def assemble_all_prompts(self): 127 | assembled_prompts = {} 128 | for prompt_key in self.config["router_prompts"].keys(): 129 | assembled_prompts[prompt_key] = self.assemble_prompt(prompt_key) 130 | return assembled_prompts 131 | 132 | variable_context = """ 133 | 134 | You are tasked with drafting contextually aware and scenario specific replies to emails. 135 | 136 | Company: Lease.ai 137 | 138 | One Liner: "Revolutionizing leasing management with AI-driven efficiency and insights." 139 | 140 | Make sure the email is personalized to the sender, addresses content relevantly and succinctly. Adopt the tone of voice and persona: 141 | 142 | Your tone of voice should be: Professional, authoritative, innovative, approachable 143 | Your character should be: Expert, reliable, forward-thinking, solution-oriented 144 | Language: Precise, engaging, informative, accessible 145 | Purpose: To streamline, optimize, and transform the leasing management process for businesses 146 | 147 | Brand Proposition: 148 | 149 | Vision: To set a new standard in leasing management through AI innovation. 150 | Mission: To empower real estate professionals with intelligent leasing solutions that drive efficiency, enhance tenant satisfaction, and maximize portfolio value. 151 | What: Lease.ai is at the forefront of transforming the real estate leasing process through advanced artificial intelligence. 152 | Value Props: Automated and optimized leasing processes, Actionable insights for decision-making, Reduced operational costs and improved efficiency, Enhanced tenant relations through personalized experiences 153 | Values: Innovation, Transparency, Reliability, Efficiency, Customer-Centricity 154 | 155 | ## Solutions/Services Offered: 156 | 157 | - A comprehensive AI-driven platform that automates the entire leasing cycle, from listing to lease management, enhancing accuracy and efficiency. 158 | - Advanced solutions focusing on the protection of sensitive data with encryption, access controls, and compliance with global data protection laws. 159 | - Customizable, on-premise AI assistants for large firms to manage their international portfolio more efficiently with personalized AI insights. 160 | 161 | ## Case Studies by Services/Solution 162 | 163 | ### Leasing Process Automation Platform 164 | 165 | - **Commercial Real Estate Streamlining with Lease.ai** 166 | - **Data Security Overhaul for a Residential Complex** 167 | - **Enhancing Property Management with AI** 168 | - Common Elements: Automated document handling and tenant screening, GDPR and CCPA compliance, Automated lease renewals and maintenance requests 169 | 170 | ### Real Estate Privacy Data Solutions 171 | 172 | - **Data Security Overhaul for a Residential Complex** 173 | - Common Elements: GDPR and CCPA compliance 174 | 175 | ### On-Prem Real Estate Gen AI Assistants for Large Firms 176 | 177 | - **Custom AI Assistant Deployment in a Global Real Estate Firm** 178 | - **Enhancing Property Management with AI** 179 | - Common Elements: Personalized AI insights, Automated lease renewals and maintenance requests 180 | 181 | 182 | ### Exhaustive Case Study Details 183 | 184 | Commercial Real Estate Streamlining with Lease.ai: [https://www.example.com/case-studies/case-study-1]: 185 | 186 | Solution: Leasing Process Automation Platform 187 | Client: Metro Commercial Properties 188 | Year: [PLACEHOLDER] 189 | 190 | Data Security Overhaul for a Residential Complex: [https://www.example.com/case-studies/case-study-2]: 191 | 192 | Solution: Real Estate Privacy Data Solutions 193 | Client: Urban Living Residential 194 | Year: [PLACEHOLDER] 195 | 196 | Custom AI Assistant Deployment in a Global Real Estate Firm: [https://www.example.com/case-studies/case-study-3]: 197 | 198 | Solution: On-Prem Real Estate Gen AI Assistants for Large Firms 199 | Client: Global Realty Inc. 200 | Year: [PLACEHOLDER] 201 | 202 | Enhancing Property Management with AI: [https://www.example.com/case-studies/case-study-4]: 203 | 204 | Solution: Leasing Process Automation Platform + On-Prem Real Estate Gen AI Assistants 205 | Client: Premier Property Management 206 | Year: [PLACEHOLDER] 207 | 208 | ## Ideal Customer Profiles 209 | 210 | 211 | ### Ideal Customer Profile (ICP) 1: Large Commercial Real Estate Firms 212 | 213 | ONE LINER DESCRIPTION: "Transform your real estate operations with Lease.ai's cutting-edge AI solutions." 214 | 215 | INDUSTRY: Commercial Real Estate 216 | 217 | GEOGRAPHY: Globally, with a strong presence in urban markets 218 | 219 | COMPANY SIZE: Large enterprises managing extensive property portfolios 220 | 221 | BUDGET: $500K+ 222 | 223 | THEY ARE BUYING: 224 | 225 | - Leasing Process Automation Platform 226 | - On-Prem Real Estate Gen AI Assistants 227 | 228 | DECISION MAKERS: 229 | 230 | - CEO, CMO, CTO, CDO 231 | - Vice President 232 | - Head Of 233 | - Director 234 | 235 | PAIN POINTS: 236 | 237 | - Inefficient leasing processes 238 | - Data privacy concerns 239 | - Need for market insights 240 | 241 | BUSINESS GOALS: 242 | 243 | - Streamline operations 244 | - Ensure data security 245 | - Gain competitive market insights 246 | 247 | TECHNOLOGIES: 248 | 249 | - AI-driven platforms 250 | - Data encryption and access controls 251 | - Customizable AI assistants 252 | 253 | ATTRIBUTES: 254 | 255 | - Forward-thinking 256 | - Data-conscious 257 | - Efficiency-focused 258 | 259 | ICP 1: Large Commercial Real Estate Firms Messaging 260 | 261 | ### ICP 1 Main Message: "Transform your real estate operations with Lease.ai's cutting-edge AI solutions. Our platform not only streamlines your leasing processes but also ensures unparalleled data security and provides the insights you need to stay ahead in the market. Let's discuss how we can elevate your property portfolio to new heights." 262 | 263 | **who we are:** Lease.ai is a pioneer in the real estate technology space, leveraging advanced AI to revolutionize leasing management. Our mission is to empower real estate professionals by automating and optimizing leasing operations, ensuring data security, and providing actionable insights to enhance tenant satisfaction and maximize portfolio value. 264 | 265 | **what we do:** We offer a comprehensive suite of AI-driven solutions designed to automate the entire leasing cycle, from listing to lease management. Our platform enhances accuracy and efficiency, ensuring data security with encryption, access controls, and compliance with global data protection laws. 266 | 267 | **how we do it:** 268 | 269 | - By deploying our Leasing Process Automation Platform, we streamline leasing processes, significantly reducing the time and effort required to manage leases. 270 | - Our Real Estate Privacy Data Solutions protect sensitive tenant and property data, ensuring compliance with GDPR, CCPA, and other data protection laws. 271 | - With our On-Prem Real Estate Gen AI Assistants for Large Firms, we provide customizable, on-premise AI solutions that offer personalized insights, enabling large firms to manage their international portfolios more efficiently. 272 | 273 | Proof Point: 274 | 275 | - [Commercial Real Estate Streamlining with Lease.ai](https://www.example.com/case-studies/case-study-1): Demonstrated a 40% reduction in lease processing time and a 25% increase in tenant satisfaction. 276 | - [Data Security Overhaul for a Residential Complex](https://www.example.com/case-studies/case-study-2): Successfully mitigated data breach risks and achieved compliance with GDPR and CCPA. 277 | - [Custom AI Assistant Deployment in a Global Real Estate Firm](https://www.example.com/case-studies/case-study-3): Enabled efficient international portfolio management, leading to a 15% increase in operational efficiency and a 20% increase in annual revenue. 278 | 279 | Example Emails: 280 | 281 | 282 | Someone we know: 283 | 284 | ``` 285 | Hi [Name], 286 | 287 | Its been a while since we last connected. Hope you've been well. I've been keeping up with your work at [Company] and I'm happy to see the progress you've made in the space. 288 | 289 | I wanted to reach out and share how Lease.ai is transforming the real estate operations of large commercial firms like yours. Our AI-driven solutions are designed to streamline leasing processes, ensure data security, and provide actionable market insights. 290 | 291 | Given your role at [Company], I believe our platform could significantly enhance your operational efficiency and tenant satisfaction. Let me know if you'd be interested in learning more about how we can tailor our solutions to meet your specific needs. 292 | 293 | Best, 294 | 295 | [Name] 296 | 297 | 298 | 299 | ``` 300 | 301 | Someone we don't know: 302 | 303 | ``` 304 | 305 | Hi [Name], 306 | 307 | My name is [First Name] [Last Name], I am the [title at Lease.ai] - we're a real estate technology company that leverages advanced AI to revolutionize leasing management for large commercial firms like yours. 308 | 309 | Our platform not only streamlines your leasing processes but also ensures unparalleled data security and provides the insights you need to stay ahead in the market. I believe that our services could greatly benefit [Company] and I'd love to discuss how we can tailor our solutions to meet your specific needs. 310 | 311 | Let me know if you'd be interested in learning more about how we can tailor our solutions to meet your specific needs. 312 | 313 | Best, 314 | 315 | [Name] 316 | 317 | 318 | ### Ideal Customer Profile (ICP) 2: Property Management Companies 319 | 320 | ONE LINER DESCRIPTION: "Revolutionize the way you manage properties with Lease.ai's AI-driven solutions." 321 | 322 | INDUSTRY: Residential and Commercial Property Management 323 | 324 | GEOGRAPHY: Nationwide, with properties in both urban and suburban areas 325 | 326 | COMPANY SIZE: Medium to large, managing multiple properties 327 | 328 | BUDGET: $200K - $500K 329 | 330 | THEY ARE BUYING: 331 | 332 | - Real Estate Privacy Data Solutions 333 | - Leasing Process Automation Platform 334 | 335 | DECISION MAKERS: 336 | 337 | - Property Manager 338 | - Operations Manager 339 | - IT Manager 340 | 341 | PAIN POINTS: 342 | 343 | - Data security and compliance 344 | - Inefficient leasing and maintenance management 345 | 346 | BUSINESS GOALS: 347 | 348 | - Enhance operational efficiency 349 | - Improve tenant satisfaction 350 | - Secure tenant data 351 | 352 | TECHNOLOGIES: 353 | 354 | - Automated leasing platforms 355 | - Data protection solutions 356 | 357 | ATTRIBUTES: 358 | 359 | - Customer-centric 360 | - Security-focused 361 | - Efficiency-driven 362 | 363 | ### ICP 2: Property Management Companies Messaging 364 | 365 | ### ICP 2 Main Message: "Lease.ai is here to revolutionize the way you manage properties. From automating tedious leasing tasks to securing your tenants' data, our solutions are designed to improve operational efficiency and enhance tenant satisfaction. Discover the potential of AI in transforming your property management approach with Lease.ai." 366 | 367 | **who we are:** Lease.ai is a leader in real estate technology, dedicated to transforming property management through the power of AI. Our innovative solutions automate leasing and maintenance processes, secure data, and provide valuable insights, enabling property managers to focus on what truly matters - tenant satisfaction and operational excellence. 368 | 369 | **what we do:** We provide a suite of AI-powered tools that automate and optimize property management tasks. From leasing automation to data security, our platforms are designed to address the unique challenges faced by property management companies, ensuring compliance and enhancing efficiency. 370 | 371 | **how we do it:** 372 | 373 | - Our Leasing Process Automation Platform automates the end-to-end leasing process, reducing manual work and increasing efficiency. 374 | - The Real Estate Privacy Data Solutions ensure tenant data is protected and compliant with the latest regulations, building trust and safeguarding your reputation. 375 | - By integrating AI-driven insights, we help property managers make informed decisions, improving property performance and tenant satisfaction. 376 | 377 | Proof Point: 378 | 379 | - [Enhancing Property Management with AI](https://www.example.com/case-studies/case-study-4): Transformed property management operations by automating lease renewals and maintenance requests, resulting in a 50% decrease in operational costs and a 35% improvement in tenant retention rates. 380 | 381 | Example Emails: 382 | 383 | Someone we know: 384 | 385 | ``` 386 | 387 | Hi [Name], 388 | 389 | I hope all is well. I've been following the developments at [Company], and it's impressive to see your growth. At Lease.ai, we've been working on solutions that could further enhance your property management efficiency and tenant satisfaction. 390 | 391 | Our AI-driven platforms are tailored for companies like yours, aiming to streamline operations and secure tenant data. I'd love to catch up and discuss how we can support [Company]'s goals. 392 | 393 | Best, 394 | [Name] 395 | 396 | 397 | ``` 398 | 399 | Someone we don't know: 400 | 401 | ``` 402 | 403 | Hello [Name], 404 | 405 | I'm [Your Name], reaching out from Lease.ai. We specialize in AI solutions for property management, focusing on automating leasing tasks and securing tenant data to enhance operational efficiency and tenant satisfaction. 406 | 407 | I believe our innovative solutions could make a significant difference for [Company]. Are you open to exploring how we can tailor our services to fit your needs? 408 | 409 | Best, 410 | [Name] 411 | 412 | 413 | ``` 414 | 415 | ### Case Study ICP Pairs 416 | 417 | ## ICP 1: Large Commercial Real Estate Firms 418 | 419 | **Commercial Real Estate Streamlining with Lease.ai:** 420 | **Summary:** This case study showcases how Lease.ai's Leasing Process Automation Platform revolutionized leasing operations for Metro Commercial Properties, leading to a 40% reduction in lease processing time and a 25% increase in tenant satisfaction. 421 | **Why They Fit:** This case study is particularly relevant to Large Commercial Real Estate Firms due to the shared challenges of managing extensive property portfolios and the need for efficient leasing processes. The solutions provided directly address these firms' pain points, such as inefficient leasing processes and the need for market insights. 422 | **How to Use in Selling:** Highlight the significant improvements in operational efficiency and tenant satisfaction achieved by Metro Commercial Properties. Emphasize the scalability of Lease.ai's solutions and their impact on reducing operational costs and enhancing portfolio value. 423 | 424 | **Custom AI Assistant Deployment in a Global Real Estate Firm:** 425 | **Summary:** Global Realty Inc. experienced a 15% increase in operational efficiency and a 20% increase in annual revenue after deploying Lease.ai's On-Prem Real Estate Gen AI Assistants. This case study demonstrates the power of personalized AI insights in managing international portfolios more efficiently. 426 | **Why They Fit:** Large Commercial Real Estate Firms often face the challenge of managing international portfolios with varying market dynamics. This case study showcases how Lease.ai's customizable AI solutions can be tailored to meet these complex needs, providing a competitive edge. 427 | **How to Use in Selling:** Discuss the tailored approach of Lease.ai's AI solutions and their ability to provide actionable insights for decision-making. Stress the benefits of improved efficiency and revenue growth as demonstrated by Global Realty Inc. 428 | 429 | ## ICP 2: Property Management Companies 430 | 431 | **Enhancing Property Management with AI:** 432 | **Summary:** Premier Property Management transformed its operations by automating lease renewals and maintenance requests with Lease.ai, resulting in a 50% decrease in operational costs and a 35% improvement in tenant retention rates. 433 | **Why They Fit:** Property Management Companies, both in residential and commercial sectors, struggle with maintaining efficient operations while ensuring tenant satisfaction. This case study directly addresses these challenges by showcasing the benefits of automating tedious tasks and leveraging AI for better decision-making. 434 | **How to Use in Selling:** Focus on the operational efficiencies and tenant satisfaction improvements achieved by Premier Property Management. Highlight how Lease.ai's solutions can be customized to address the specific needs of property management companies, leading to significant cost savings and improved tenant relations. 435 | 436 | **Data Security Overhaul for a Residential Complex:** 437 | **Summary:** Urban Living Residential achieved GDPR and CCPA compliance and mitigated data breach risks after implementing Lease.ai's Real Estate Privacy Data Solutions. This case study highlights the importance of data security in property management. 438 | **Why They Fit:** With increasing concerns over data privacy and compliance, Property Management Companies need robust solutions to protect tenant data. This case study is relevant as it demonstrates Lease.ai's capability to enhance data security and ensure regulatory compliance. 439 | **How to Use in Selling:** Emphasize the growing importance of data security in the real estate sector and how Lease.ai's solutions address these critical needs. Point out the peace of mind and trust that comes with ensuring tenant data is protected and compliant with regulations. 440 | 441 | 442 | --- 443 | 444 | You should use the above context to ensure that all your replies are contextually relevant, accurately represent the Organization's brand and offerings and represent the company in the highest light while personally addressing the sender’s message. 445 | 446 | When referencing a case study or other link, you should use the designated [PLACEHOLDER] signified by the [case study] or [here] for the hubspot scheduling link (exact match, always all proper capitalization case) placeholders. 447 | 448 | Offer the scheduling link when a lead inquries about learning more, requests a meeting, or otherwise makes sense. THE SCHEDULING LINK MUST BE INSERTED VIA [PLACEHOLDER] [here] - YOU MUST ALWAYS USE THIS [PLACEHOLDER]. 449 | 450 | You are to answer questions regarding examples of work, Organization's brand proposition, services provided and assist with scheduling. You are not authorized to make legally binding agreements, provide information on service costs, timelines or other sensitive information. Never answer contract questions, negotiate or otherwise provide sensitive information. 451 | 452 | Never bombard a recipient with too much context, use your best judgment to provide the minimum necessary context to get them excited about Organization without risking information overwhelm. 453 | 454 | DO NOT PRINT IN MARKDOWN FORMATTING, PRINT IN PLAIN TEXT. NEVER WRITE A SIGNATURE, IT WILL BE PRESET. DO NOT WRITE A SIGNATURE. 455 | """ 456 | 457 | # Assuming common_context and variable_context are defined above this snippet and contain the necessary text 458 | 459 | drafting_config = { 460 | "variable_context": variable_context, 461 | "signature_block": """ 462 | --- 463 | Jordan Smith 464 | Head of Sales 465 | Lease.ai 466 | 467 | lease.ai 468 | """, 469 | "links_for_insertion": { 470 | # Define the placeholders and their corresponding links 471 | "[CASE STUDY NAME 1]": "https://www.example.com/case-studies/case-study-1", 472 | "[CASE STUDY NAME 2]": "https://www.example.com/case-studies/case-study-2", 473 | "[CASE STUDY NAME 3]": "https://www.example.com/case-studies/case-study-3", 474 | "[CASE STUDY NAME 4]": "https://www.example.com/case-studies/case-study-4", 475 | "[here]": "https://meetings.hubspot.com/jordan-smith/lease-ai-demo-call-30-min" 476 | # ... add all necessary links here 477 | } 478 | } 479 | 480 | sending_manager_config = { 481 | "sending_email_address": "jordan.smith@lease.ai", 482 | "whitelisted_domains": { 483 | "enabled": 0, # 0 for off, 1 for on 484 | "domains": ["lease.ai", "example.com"] 485 | }, 486 | "draft_or_autosend": 0 # 0 for draft, 1 for autosend 487 | } 488 | 489 | def handler(pd: "pipedream"): 490 | 491 | # Assuming `semantic_routers_config` is your main configuration dictionary 492 | prompt_assembler = PromptAssembler(semantic_routers_config) 493 | 494 | # Assemble all prompts based on the current configuration 495 | assembled_prompts = prompt_assembler.assemble_all_prompts() 496 | 497 | # Export the assembled prompts for use in downstream steps 498 | pd.export("assembled_prompts", assembled_prompts) 499 | print("Config and Prompts Exported.") 500 | 501 | # Export the configuration for use in downstream steps 502 | pd.export("semantic_routers_config", semantic_routers_config) 503 | pd.export("drafting_config", drafting_config) 504 | pd.export("sending_manager_config", sending_manager_config) 505 | print("Config and Prompts Exported.") 506 | #pd.export("hubspot_crm_config", hubspot_crm_config) 507 | 508 | -------------------------------------------------------------------------------- /workflow/.workflow.state: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE 2 | --- 3 | schema: workflow.state/2022.04 4 | id: p_G6Cxrry 5 | 6 | -------------------------------------------------------------------------------- /workflow/0_workflow_config/entry.py: -------------------------------------------------------------------------------- 1 | # Define your org's configuration and custom settings 2 | semantic_routers_config = { 3 | "org_name": "Lease.ai", 4 | # past in your own label list created by the label setup script here 5 | "email_labels": { 6 | "EmailClassification": {"NOT_FROM_REAL_PERSON": "Label_7"}, 7 | "EmailRelevancy": {"NON_PROSPECTING_RELATED": "Label_8"}, 8 | "EmailSentimentAndFunnelStage": { 9 | "LEAD": "Label_9", 10 | "INTERESTED": "Label_10", 11 | "QUALIFIED_TO_BUY": "Label_11", 12 | "APPOINTMENT_SCHEDULED": "Label_12", 13 | "PRESENTATION_SCHEDULED": "Label_13", 14 | "DECISION_MAKER_BOUGHT_IN": "Label_14", 15 | "CONTRACT_SENT": "Label_15", 16 | "CLOSED_WON": "Label_16" 17 | }, 18 | "EmailScenarioInquiryType": { 19 | "COLD_OUTBOUND_REPLY": "Label_17", 20 | "WARM_INTRO_REPLY": "Label_18", 21 | "ORGANIC_INBOUND": "Label_19" 22 | }, 23 | "EmailScenarioSenderCategory": { 24 | "ICP 1: Large Commercial Real Estate Firms": "Label_20", 25 | "ICP 2: Property Management Companies": "Label_21", 26 | "ICP_OTHER": "Label_22" 27 | } 28 | }, 29 | "router_prompts": { 30 | "email_classification": """ 31 | Function: Email Classification 32 | Analyze the email and determine whether it requires a response or not. 33 | 34 | Ruleset: 35 | - Can either be 0 or 1. 36 | - 0 if the email is a notification, a billing reminder, a social media update, spam, cold sales email or some other sort of email that a person would not generally respond to. 37 | - 1 if the email is from a real person with a genuine inquiry that requires some sort of response. Specific notification exceptions for [example@example.com], as [reasoning]. 38 | """, 39 | "email_relevancy": """ 40 | Function: Email Relevancy Classification 41 | Analyze the email and determine whether it is relevant to the Organization inbox. 42 | 43 | Ruleset: 44 | - Can either be 0 or 1. 45 | - 0 if the email is not relevant to the Organization prospecting inbox. For example, if the email is part of an ongoing chain that the inbox was cc'd on but does not require a response or action, is an inbound cold sales email, related to a current ongoing project, or company operations. Even if the sender sends a personalized cold outreach message, they are not relevant to this outbound sales system. 46 | - 1 if the email is relevant to the Organization's sales and lead generation inbox. For example, if the email is a direct inquiry, a sales question, a lead trying to schedule a call, or requires some sort of action from the Organization inbox. 47 | """, 48 | "email_sensitivity": """ 49 | Function: Email Sensitivity Classification 50 | Analyze the email and determine whether it is sensitive and requires stakeholder attention. Can either be 0 or 1. 0 if the email is not sensitive, 1 if the email is sensitive and requires stakeholder attention. Sensitive emails include any regarding pricing, requests for a contract, contract negotiations, client complaints, project scoping, or basically any email where an SDR would pass off the email chain to legal or high executives. It does not pertain to questions around product offerings, or any questions that could be answered referencing a company brand book or plays playbook. 51 | 52 | Ruleset: 53 | - Can either be 0 or 1. 54 | - 0 if the email is not sensitive. 55 | - 1 if the email is sensitive and requires special treatment and/or stakeholder loop in. 56 | """, 57 | "email_sentiment_and_funnel_stage": """ 58 | Function: Sentiment and Funnel Stage Classification 59 | Determine the sentiment and stage of the sales funnel the email corresponds to. 60 | 61 | Ruleset: 62 | - Use the content of the email to identify keywords or phrases that indicate sentiment and the stage of the sales funnel. 63 | - Labels can include: {EmailSentimentAndFunnelStage} 64 | """, 65 | "email_scenario": """ 66 | Function: Email Scenario Identification 67 | Identify the type of inquiry, the ICP category of the sender, and any specific details related to the inquiry. 68 | 69 | Ruleset: 70 | - types of inquiry options: {EmailScenarioInquiryType} 71 | - sender category options: {EmailScenarioSenderCategory} 72 | """ 73 | }, 74 | "inquiry_types": "{EmailScenarioInquiryType}", 75 | "sender_categories": "{EmailScenarioSenderCategory}", 76 | 77 | "prompt_label_mapping": { 78 | "email_classification": ["EmailClassification"], 79 | "email_relevancy": ["EmailRelevancy"], 80 | "email_sentiment_and_funnel_stage": ["EmailSentimentAndFunnelStage"], 81 | "email_scenario": ["EmailScenarioInquiryType", "EmailScenarioSenderCategory"], 82 | "inquiry_types": ["EmailScenarioInquiryType"], 83 | "sender_categories": ["EmailScenarioSenderCategory"] 84 | # Add other mappings as necessary 85 | } 86 | } 87 | 88 | 89 | common_context = """ 90 | You are monitoring an Organization's primary sales, lead generation and general inbound email account. You have one task as a part of a larger system 91 | which will be described below. For context the larger system designed to provide an automated, intelligent solution for managing emails at the Organization. It is comprised of: 92 | 1. Email Classification (is this email from a real person or a notification/spam?) 93 | 2. Email Scenario Identification (what is the nature of the email? Who is it from? What is it about?) 94 | 3. Funnel Stage Classification (what stage of the sales funnel does this email seem like?) 95 | 4. Sensitivity Determination and Stakeholder Notification (if the email is from a real person, should a stakeholder handle it?) 96 | 97 | Do your best to address the task below in the context of the larger system. Prioritize accuracy, and think quietly to yourself before responding. 98 | 99 | YOUR ACTIONS MUST STRICTLY FOLLOW THE PROVIDED RULESETS IN EACH FUNCTION. IF YOU GENERATE ANYTHING OUTSIDE OF THE RULESET, YOU FAIL THE TEST. 100 | 101 | You are responsible for the below task: 102 | """ 103 | 104 | class PromptAssembler: 105 | def __init__(self, config): 106 | self.config = config 107 | 108 | def assemble_prompt(self, prompt_key, **kwargs): 109 | template = self.config["router_prompts"][prompt_key] 110 | label_categories = self.config["prompt_label_mapping"].get(prompt_key, []) 111 | 112 | for category in label_categories: 113 | if category in self.config["email_labels"]: 114 | # Concatenate all label names (keys) into a single string 115 | labels_string = ", ".join(self.config["email_labels"][category].keys()) 116 | # Replace the placeholder with this string of names 117 | template = template.replace(f"{{{category}}}", labels_string) 118 | elif category == "inquiry_types" or category == "sender_categories": 119 | # Directly use the string for inquiry_types and sender_categories without looking up labels 120 | template = template.replace(f"{{{category}}}", self.config[category]) 121 | 122 | # Combine each prompt with the common_context 123 | combined_template = common_context + template 124 | return combined_template 125 | 126 | def assemble_all_prompts(self): 127 | assembled_prompts = {} 128 | for prompt_key in self.config["router_prompts"].keys(): 129 | assembled_prompts[prompt_key] = self.assemble_prompt(prompt_key) 130 | return assembled_prompts 131 | 132 | variable_context = """ 133 | 134 | You are tasked with drafting contextually aware and scenario specific replies to emails. 135 | 136 | Company: Lease.ai 137 | 138 | One Liner: "Revolutionizing leasing management with AI-driven efficiency and insights." 139 | 140 | Make sure the email is personalized to the sender, addresses content relevantly and succinctly. Adopt the tone of voice and persona: 141 | 142 | Your tone of voice should be: Professional, authoritative, innovative, approachable 143 | Your character should be: Expert, reliable, forward-thinking, solution-oriented 144 | Language: Precise, engaging, informative, accessible 145 | Purpose: To streamline, optimize, and transform the leasing management process for businesses 146 | 147 | Brand Proposition: 148 | 149 | Vision: To set a new standard in leasing management through AI innovation. 150 | Mission: To empower real estate professionals with intelligent leasing solutions that drive efficiency, enhance tenant satisfaction, and maximize portfolio value. 151 | What: Lease.ai is at the forefront of transforming the real estate leasing process through advanced artificial intelligence. 152 | Value Props: Automated and optimized leasing processes, Actionable insights for decision-making, Reduced operational costs and improved efficiency, Enhanced tenant relations through personalized experiences 153 | Values: Innovation, Transparency, Reliability, Efficiency, Customer-Centricity 154 | 155 | ## Solutions/Services Offered: 156 | 157 | - A comprehensive AI-driven platform that automates the entire leasing cycle, from listing to lease management, enhancing accuracy and efficiency. 158 | - Advanced solutions focusing on the protection of sensitive data with encryption, access controls, and compliance with global data protection laws. 159 | - Customizable, on-premise AI assistants for large firms to manage their international portfolio more efficiently with personalized AI insights. 160 | 161 | ## Case Studies by Services/Solution 162 | 163 | ### Leasing Process Automation Platform 164 | 165 | - **Commercial Real Estate Streamlining with Lease.ai** 166 | - **Data Security Overhaul for a Residential Complex** 167 | - **Enhancing Property Management with AI** 168 | - Common Elements: Automated document handling and tenant screening, GDPR and CCPA compliance, Automated lease renewals and maintenance requests 169 | 170 | ### Real Estate Privacy Data Solutions 171 | 172 | - **Data Security Overhaul for a Residential Complex** 173 | - Common Elements: GDPR and CCPA compliance 174 | 175 | ### On-Prem Real Estate Gen AI Assistants for Large Firms 176 | 177 | - **Custom AI Assistant Deployment in a Global Real Estate Firm** 178 | - **Enhancing Property Management with AI** 179 | - Common Elements: Personalized AI insights, Automated lease renewals and maintenance requests 180 | 181 | 182 | ### Exhaustive Case Study Details 183 | 184 | Commercial Real Estate Streamlining with Lease.ai: [https://www.example.com/case-studies/case-study-1]: 185 | 186 | Solution: Leasing Process Automation Platform 187 | Client: Metro Commercial Properties 188 | Year: [PLACEHOLDER] 189 | 190 | Data Security Overhaul for a Residential Complex: [https://www.example.com/case-studies/case-study-2]: 191 | 192 | Solution: Real Estate Privacy Data Solutions 193 | Client: Urban Living Residential 194 | Year: [PLACEHOLDER] 195 | 196 | Custom AI Assistant Deployment in a Global Real Estate Firm: [https://www.example.com/case-studies/case-study-3]: 197 | 198 | Solution: On-Prem Real Estate Gen AI Assistants for Large Firms 199 | Client: Global Realty Inc. 200 | Year: [PLACEHOLDER] 201 | 202 | Enhancing Property Management with AI: [https://www.example.com/case-studies/case-study-4]: 203 | 204 | Solution: Leasing Process Automation Platform + On-Prem Real Estate Gen AI Assistants 205 | Client: Premier Property Management 206 | Year: [PLACEHOLDER] 207 | 208 | ## Ideal Customer Profiles 209 | 210 | 211 | ### Ideal Customer Profile (ICP) 1: Large Commercial Real Estate Firms 212 | 213 | ONE LINER DESCRIPTION: "Transform your real estate operations with Lease.ai's cutting-edge AI solutions." 214 | 215 | INDUSTRY: Commercial Real Estate 216 | 217 | GEOGRAPHY: Globally, with a strong presence in urban markets 218 | 219 | COMPANY SIZE: Large enterprises managing extensive property portfolios 220 | 221 | BUDGET: $500K+ 222 | 223 | THEY ARE BUYING: 224 | 225 | - Leasing Process Automation Platform 226 | - On-Prem Real Estate Gen AI Assistants 227 | 228 | DECISION MAKERS: 229 | 230 | - CEO, CMO, CTO, CDO 231 | - Vice President 232 | - Head Of 233 | - Director 234 | 235 | PAIN POINTS: 236 | 237 | - Inefficient leasing processes 238 | - Data privacy concerns 239 | - Need for market insights 240 | 241 | BUSINESS GOALS: 242 | 243 | - Streamline operations 244 | - Ensure data security 245 | - Gain competitive market insights 246 | 247 | TECHNOLOGIES: 248 | 249 | - AI-driven platforms 250 | - Data encryption and access controls 251 | - Customizable AI assistants 252 | 253 | ATTRIBUTES: 254 | 255 | - Forward-thinking 256 | - Data-conscious 257 | - Efficiency-focused 258 | 259 | ICP 1: Large Commercial Real Estate Firms Messaging 260 | 261 | ### ICP 1 Main Message: "Transform your real estate operations with Lease.ai's cutting-edge AI solutions. Our platform not only streamlines your leasing processes but also ensures unparalleled data security and provides the insights you need to stay ahead in the market. Let's discuss how we can elevate your property portfolio to new heights." 262 | 263 | **who we are:** Lease.ai is a pioneer in the real estate technology space, leveraging advanced AI to revolutionize leasing management. Our mission is to empower real estate professionals by automating and optimizing leasing operations, ensuring data security, and providing actionable insights to enhance tenant satisfaction and maximize portfolio value. 264 | 265 | **what we do:** We offer a comprehensive suite of AI-driven solutions designed to automate the entire leasing cycle, from listing to lease management. Our platform enhances accuracy and efficiency, ensuring data security with encryption, access controls, and compliance with global data protection laws. 266 | 267 | **how we do it:** 268 | 269 | - By deploying our Leasing Process Automation Platform, we streamline leasing processes, significantly reducing the time and effort required to manage leases. 270 | - Our Real Estate Privacy Data Solutions protect sensitive tenant and property data, ensuring compliance with GDPR, CCPA, and other data protection laws. 271 | - With our On-Prem Real Estate Gen AI Assistants for Large Firms, we provide customizable, on-premise AI solutions that offer personalized insights, enabling large firms to manage their international portfolios more efficiently. 272 | 273 | Proof Point: 274 | 275 | - [Commercial Real Estate Streamlining with Lease.ai](https://www.example.com/case-studies/case-study-1): Demonstrated a 40% reduction in lease processing time and a 25% increase in tenant satisfaction. 276 | - [Data Security Overhaul for a Residential Complex](https://www.example.com/case-studies/case-study-2): Successfully mitigated data breach risks and achieved compliance with GDPR and CCPA. 277 | - [Custom AI Assistant Deployment in a Global Real Estate Firm](https://www.example.com/case-studies/case-study-3): Enabled efficient international portfolio management, leading to a 15% increase in operational efficiency and a 20% increase in annual revenue. 278 | 279 | Example Emails: 280 | 281 | 282 | Someone we know: 283 | 284 | ``` 285 | Hi [Name], 286 | 287 | Its been a while since we last connected. Hope you've been well. I've been keeping up with your work at [Company] and I'm happy to see the progress you've made in the space. 288 | 289 | I wanted to reach out and share how Lease.ai is transforming the real estate operations of large commercial firms like yours. Our AI-driven solutions are designed to streamline leasing processes, ensure data security, and provide actionable market insights. 290 | 291 | Given your role at [Company], I believe our platform could significantly enhance your operational efficiency and tenant satisfaction. Let me know if you'd be interested in learning more about how we can tailor our solutions to meet your specific needs. 292 | 293 | Best, 294 | 295 | [Name] 296 | 297 | 298 | 299 | ``` 300 | 301 | Someone we don't know: 302 | 303 | ``` 304 | 305 | Hi [Name], 306 | 307 | My name is [First Name] [Last Name], I am the [title at Lease.ai] - we're a real estate technology company that leverages advanced AI to revolutionize leasing management for large commercial firms like yours. 308 | 309 | Our platform not only streamlines your leasing processes but also ensures unparalleled data security and provides the insights you need to stay ahead in the market. I believe that our services could greatly benefit [Company] and I'd love to discuss how we can tailor our solutions to meet your specific needs. 310 | 311 | Let me know if you'd be interested in learning more about how we can tailor our solutions to meet your specific needs. 312 | 313 | Best, 314 | 315 | [Name] 316 | 317 | 318 | ### Ideal Customer Profile (ICP) 2: Property Management Companies 319 | 320 | ONE LINER DESCRIPTION: "Revolutionize the way you manage properties with Lease.ai's AI-driven solutions." 321 | 322 | INDUSTRY: Residential and Commercial Property Management 323 | 324 | GEOGRAPHY: Nationwide, with properties in both urban and suburban areas 325 | 326 | COMPANY SIZE: Medium to large, managing multiple properties 327 | 328 | BUDGET: $200K - $500K 329 | 330 | THEY ARE BUYING: 331 | 332 | - Real Estate Privacy Data Solutions 333 | - Leasing Process Automation Platform 334 | 335 | DECISION MAKERS: 336 | 337 | - Property Manager 338 | - Operations Manager 339 | - IT Manager 340 | 341 | PAIN POINTS: 342 | 343 | - Data security and compliance 344 | - Inefficient leasing and maintenance management 345 | 346 | BUSINESS GOALS: 347 | 348 | - Enhance operational efficiency 349 | - Improve tenant satisfaction 350 | - Secure tenant data 351 | 352 | TECHNOLOGIES: 353 | 354 | - Automated leasing platforms 355 | - Data protection solutions 356 | 357 | ATTRIBUTES: 358 | 359 | - Customer-centric 360 | - Security-focused 361 | - Efficiency-driven 362 | 363 | ### ICP 2: Property Management Companies Messaging 364 | 365 | ### ICP 2 Main Message: "Lease.ai is here to revolutionize the way you manage properties. From automating tedious leasing tasks to securing your tenants' data, our solutions are designed to improve operational efficiency and enhance tenant satisfaction. Discover the potential of AI in transforming your property management approach with Lease.ai." 366 | 367 | **who we are:** Lease.ai is a leader in real estate technology, dedicated to transforming property management through the power of AI. Our innovative solutions automate leasing and maintenance processes, secure data, and provide valuable insights, enabling property managers to focus on what truly matters - tenant satisfaction and operational excellence. 368 | 369 | **what we do:** We provide a suite of AI-powered tools that automate and optimize property management tasks. From leasing automation to data security, our platforms are designed to address the unique challenges faced by property management companies, ensuring compliance and enhancing efficiency. 370 | 371 | **how we do it:** 372 | 373 | - Our Leasing Process Automation Platform automates the end-to-end leasing process, reducing manual work and increasing efficiency. 374 | - The Real Estate Privacy Data Solutions ensure tenant data is protected and compliant with the latest regulations, building trust and safeguarding your reputation. 375 | - By integrating AI-driven insights, we help property managers make informed decisions, improving property performance and tenant satisfaction. 376 | 377 | Proof Point: 378 | 379 | - [Enhancing Property Management with AI](https://www.example.com/case-studies/case-study-4): Transformed property management operations by automating lease renewals and maintenance requests, resulting in a 50% decrease in operational costs and a 35% improvement in tenant retention rates. 380 | 381 | Example Emails: 382 | 383 | Someone we know: 384 | 385 | ``` 386 | 387 | Hi [Name], 388 | 389 | I hope all is well. I've been following the developments at [Company], and it's impressive to see your growth. At Lease.ai, we've been working on solutions that could further enhance your property management efficiency and tenant satisfaction. 390 | 391 | Our AI-driven platforms are tailored for companies like yours, aiming to streamline operations and secure tenant data. I'd love to catch up and discuss how we can support [Company]'s goals. 392 | 393 | Best, 394 | [Name] 395 | 396 | 397 | ``` 398 | 399 | Someone we don't know: 400 | 401 | ``` 402 | 403 | Hello [Name], 404 | 405 | I'm [Your Name], reaching out from Lease.ai. We specialize in AI solutions for property management, focusing on automating leasing tasks and securing tenant data to enhance operational efficiency and tenant satisfaction. 406 | 407 | I believe our innovative solutions could make a significant difference for [Company]. Are you open to exploring how we can tailor our services to fit your needs? 408 | 409 | Best, 410 | [Name] 411 | 412 | 413 | ``` 414 | 415 | ### Case Study ICP Pairs 416 | 417 | ## ICP 1: Large Commercial Real Estate Firms 418 | 419 | **Commercial Real Estate Streamlining with Lease.ai:** 420 | **Summary:** This case study showcases how Lease.ai's Leasing Process Automation Platform revolutionized leasing operations for Metro Commercial Properties, leading to a 40% reduction in lease processing time and a 25% increase in tenant satisfaction. 421 | **Why They Fit:** This case study is particularly relevant to Large Commercial Real Estate Firms due to the shared challenges of managing extensive property portfolios and the need for efficient leasing processes. The solutions provided directly address these firms' pain points, such as inefficient leasing processes and the need for market insights. 422 | **How to Use in Selling:** Highlight the significant improvements in operational efficiency and tenant satisfaction achieved by Metro Commercial Properties. Emphasize the scalability of Lease.ai's solutions and their impact on reducing operational costs and enhancing portfolio value. 423 | 424 | **Custom AI Assistant Deployment in a Global Real Estate Firm:** 425 | **Summary:** Global Realty Inc. experienced a 15% increase in operational efficiency and a 20% increase in annual revenue after deploying Lease.ai's On-Prem Real Estate Gen AI Assistants. This case study demonstrates the power of personalized AI insights in managing international portfolios more efficiently. 426 | **Why They Fit:** Large Commercial Real Estate Firms often face the challenge of managing international portfolios with varying market dynamics. This case study showcases how Lease.ai's customizable AI solutions can be tailored to meet these complex needs, providing a competitive edge. 427 | **How to Use in Selling:** Discuss the tailored approach of Lease.ai's AI solutions and their ability to provide actionable insights for decision-making. Stress the benefits of improved efficiency and revenue growth as demonstrated by Global Realty Inc. 428 | 429 | ## ICP 2: Property Management Companies 430 | 431 | **Enhancing Property Management with AI:** 432 | **Summary:** Premier Property Management transformed its operations by automating lease renewals and maintenance requests with Lease.ai, resulting in a 50% decrease in operational costs and a 35% improvement in tenant retention rates. 433 | **Why They Fit:** Property Management Companies, both in residential and commercial sectors, struggle with maintaining efficient operations while ensuring tenant satisfaction. This case study directly addresses these challenges by showcasing the benefits of automating tedious tasks and leveraging AI for better decision-making. 434 | **How to Use in Selling:** Focus on the operational efficiencies and tenant satisfaction improvements achieved by Premier Property Management. Highlight how Lease.ai's solutions can be customized to address the specific needs of property management companies, leading to significant cost savings and improved tenant relations. 435 | 436 | **Data Security Overhaul for a Residential Complex:** 437 | **Summary:** Urban Living Residential achieved GDPR and CCPA compliance and mitigated data breach risks after implementing Lease.ai's Real Estate Privacy Data Solutions. This case study highlights the importance of data security in property management. 438 | **Why They Fit:** With increasing concerns over data privacy and compliance, Property Management Companies need robust solutions to protect tenant data. This case study is relevant as it demonstrates Lease.ai's capability to enhance data security and ensure regulatory compliance. 439 | **How to Use in Selling:** Emphasize the growing importance of data security in the real estate sector and how Lease.ai's solutions address these critical needs. Point out the peace of mind and trust that comes with ensuring tenant data is protected and compliant with regulations. 440 | 441 | 442 | --- 443 | 444 | You should use the above context to ensure that all your replies are contextually relevant, accurately represent the Organization's brand and offerings and represent the company in the highest light while personally addressing the sender’s message. 445 | 446 | When referencing a case study or other link, you should use the designated [PLACEHOLDER] signified by the [case study] or [here] for the hubspot scheduling link (exact match, always all proper capitalization case) placeholders. 447 | 448 | Offer the scheduling link when a lead inquries about learning more, requests a meeting, or otherwise makes sense. THE SCHEDULING LINK MUST BE INSERTED VIA [PLACEHOLDER] [here] - YOU MUST ALWAYS USE THIS [PLACEHOLDER]. 449 | 450 | You are to answer questions regarding examples of work, Organization's brand proposition, services provided and assist with scheduling. You are not authorized to make legally binding agreements, provide information on service costs, timelines or other sensitive information. Never answer contract questions, negotiate or otherwise provide sensitive information. 451 | 452 | Never bombard a recipient with too much context, use your best judgment to provide the minimum necessary context to get them excited about Organization without risking information overwhelm. 453 | 454 | DO NOT PRINT IN MARKDOWN FORMATTING, PRINT IN PLAIN TEXT. NEVER WRITE A SIGNATURE, IT WILL BE PRESET. DO NOT WRITE A SIGNATURE. 455 | """ 456 | 457 | # Assuming common_context and variable_context are defined above this snippet and contain the necessary text 458 | 459 | drafting_config = { 460 | "variable_context": variable_context, 461 | "signature_block": """ 462 | --- 463 | Jordan Smith 464 | Head of Sales 465 | Lease.ai 466 | 467 | lease.ai 468 | """, 469 | "links_for_insertion": { 470 | # Define the placeholders and their corresponding links 471 | "[CASE STUDY NAME 1]": "https://www.example.com/case-studies/case-study-1", 472 | "[CASE STUDY NAME 2]": "https://www.example.com/case-studies/case-study-2", 473 | "[CASE STUDY NAME 3]": "https://www.example.com/case-studies/case-study-3", 474 | "[CASE STUDY NAME 4]": "https://www.example.com/case-studies/case-study-4", 475 | "[here]": "https://meetings.hubspot.com/jordan-smith/lease-ai-demo-call-30-min" 476 | # ... add all necessary links here 477 | } 478 | } 479 | 480 | sending_manager_config = { 481 | "sending_email_address": "jordan.smith@lease.ai", 482 | "whitelisted_domains": { 483 | "enabled": 0, # 0 for off, 1 for on 484 | "domains": ["lease.ai", "example.com"] 485 | }, 486 | "draft_or_autosend": 0 # 0 for draft, 1 for autosend 487 | } 488 | 489 | def handler(pd: "pipedream"): 490 | 491 | # Assuming `semantic_routers_config` is your main configuration dictionary 492 | prompt_assembler = PromptAssembler(semantic_routers_config) 493 | 494 | # Assemble all prompts based on the current configuration 495 | assembled_prompts = prompt_assembler.assemble_all_prompts() 496 | 497 | # Export the assembled prompts for use in downstream steps 498 | pd.export("assembled_prompts", assembled_prompts) 499 | print("Config and Prompts Exported.") 500 | 501 | # Export the configuration for use in downstream steps 502 | pd.export("semantic_routers_config", semantic_routers_config) 503 | pd.export("drafting_config", drafting_config) 504 | pd.export("sending_manager_config", sending_manager_config) 505 | print("Config and Prompts Exported.") 506 | #pd.export("hubspot_crm_config", hubspot_crm_config) 507 | 508 | -------------------------------------------------------------------------------- /workflow/1_parse_thread/entry.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import re 3 | import requests 4 | from bs4 import BeautifulSoup 5 | from dateutil.parser import parse 6 | 7 | 8 | class EmailParser: 9 | def __init__(self, payload): 10 | self.payload = payload 11 | print("EmailParser initialized with payload.") 12 | 13 | def extract_details(self): 14 | try: 15 | sender_value = self.get_header_value("From") 16 | recipient_value = self.get_header_value("To") 17 | subject_value = self.get_header_value("Subject") 18 | date_value = self.get_header_value("Date") 19 | 20 | sender = self.extract_email_address(sender_value) if sender_value else None 21 | recipients = [self.extract_email_address(email) for email in recipient_value.split(',')] if recipient_value else None 22 | subject = subject_value if subject_value else None 23 | date = parse(date_value) if date_value else None 24 | 25 | return sender, recipients, subject, date 26 | except Exception as e: 27 | print(f"Error extracting email details: {e}") 28 | return None, None, None, None 29 | 30 | def process_content(self, content_type, raw_content): 31 | try: 32 | if content_type == "text/html": 33 | cleaned_content = self.remove_html(raw_content) 34 | else: 35 | cleaned_content = raw_content 36 | 37 | cleaned_content = self.remove_quoted_content(cleaned_content) 38 | return cleaned_content.strip() 39 | except Exception as e: 40 | print(f"Error processing email content: {e}") 41 | return "" 42 | 43 | def remove_html(self, html_content): 44 | try: 45 | soup = BeautifulSoup(html_content, "html.parser") 46 | return soup.get_text() 47 | except Exception as e: 48 | print(f"Error removing HTML content: {e}") 49 | return html_content 50 | 51 | def remove_quoted_content(self, content, content_type='text/plain'): 52 | if content_type == 'text/plain': 53 | return self.remove_quoted_content_plain(content) 54 | elif content_type == 'text/html': 55 | return self.remove_quoted_content_html(content) 56 | else: 57 | raise ValueError('Unsupported content type: ' + content_type) 58 | 59 | def remove_quoted_content_plain(self, content): 60 | # Define regex patterns for quoted lines and splitter patterns 61 | quoted_line_pattern = re.compile(r'^\s*>+.*$', re.MULTILINE) 62 | splitter_pattern = re.compile( 63 | r'^\s*[-]+[ ]*Forwarded message[ ]*[-]+\s*$|' 64 | r'^\s*[-]+[ ]*Original Message[ ]*[-]+\s*$|' 65 | r'^\s*On.*wrote:$|' 66 | r'^\s*From:.*$|' 67 | r'^\s*Sent:.*$|' 68 | r'^\s*To:.*$|' 69 | r'^\s*Subject:.*$', re.MULTILINE | re.IGNORECASE) 70 | 71 | # Remove quoted lines 72 | content = re.sub(quoted_line_pattern, '', content) 73 | # Remove splitter patterns 74 | content = re.sub(splitter_pattern, '', content) 75 | # Clean up the content 76 | content = self.clean_up_content(content) 77 | return content 78 | 79 | def remove_quoted_content_html(self, content): 80 | # Parse the HTML content with BeautifulSoup 81 | soup = BeautifulSoup(content, 'html.parser') 82 | # Remove blockquote tags and other common quoting structures 83 | for blockquote in soup.find_all('blockquote'): 84 | blockquote.decompose() 85 | # Convert back to string and clean up the content 86 | content = str(soup) 87 | content = self.clean_up_content(content) 88 | return content 89 | 90 | def clean_up_content(self, content): 91 | # Trim excessive whitespace and newlines 92 | content = re.sub(r'\n\s*\n', '\n\n', content) # Reduce multiple newlines to double newlines 93 | content = content.strip() # Remove leading and trailing whitespace 94 | return content 95 | 96 | def get_header_value(self, header_name): 97 | try: 98 | for header in self.payload.get("headers", []): 99 | if header["name"].lower() == header_name.lower(): 100 | return header["value"] 101 | return None 102 | except Exception as e: 103 | print(f"Error getting header value for {header_name}: {e}") 104 | return None 105 | 106 | def extract_email_address(self, string): 107 | try: 108 | email_regex = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' 109 | result = re.search(email_regex, string) 110 | return result.group(0) if result else "" 111 | except Exception as e: 112 | print(f"Error extracting email address from string: {e}") 113 | return "" 114 | 115 | class GmailAPI: 116 | def __init__(self, headers): 117 | self.headers = headers 118 | print("GmailAPI initialized with headers.") 119 | 120 | def get_thread_messages(self, thread_id): 121 | try: 122 | url = f"https://gmail.googleapis.com/gmail/v1/users/me/threads/{thread_id}" 123 | response = requests.get(url, headers=self.headers) 124 | response.raise_for_status() # Will raise an HTTPError if the HTTP request returned an unsuccessful status code 125 | messages = response.json().get("messages", []) 126 | return [(message["id"], message.get("labelIds", [])) for message in messages] 127 | except requests.HTTPError as e: 128 | print(f"HTTPError while getting thread messages: {e}") 129 | raise 130 | except Exception as e: 131 | print(f"Error while getting thread messages: {e}") 132 | raise 133 | 134 | def get_message_data(self, message_id): 135 | try: 136 | url = f"https://gmail.googleapis.com/gmail/v1/users/me/messages/{message_id}" 137 | response = requests.get(url, headers=self.headers) 138 | response.raise_for_status() 139 | message = response.json() 140 | message_payload = message.get('payload', {}) 141 | content_type = message_payload.get('mimeType', '') 142 | 143 | # Check if the content type is 'multipart/report' which is used for DSNs 144 | if content_type == 'multipart/report': 145 | print(f"Message ID {message_id} is a delivery status notification. Skipping.") 146 | return None 147 | 148 | raw_content = None 149 | if 'parts' in message_payload: 150 | for part in message_payload['parts']: 151 | if part['mimeType'] in ['text/plain', 'text/html']: 152 | content_type = part['mimeType'] 153 | raw_content = base64.urlsafe_b64decode(part['body']['data']).decode("utf-8") 154 | break 155 | elif 'mimeType' in message_payload and 'body' in message_payload and 'data' in message_payload['body']: 156 | content_type = message_payload.get('mimeType', 'text/plain') 157 | raw_content = base64.urlsafe_b64decode(message_payload['body']['data']).decode("utf-8") 158 | 159 | if raw_content is None: 160 | print(f"No content found for message ID {message_id}. Skipping.") 161 | return None 162 | 163 | email_parser = EmailParser({'headers': message_payload.get('headers', [])}) 164 | processed_content = email_parser.process_content(content_type, raw_content) 165 | 166 | return { 167 | 'sender': email_parser.extract_email_address(email_parser.get_header_value('From')), 168 | 'recipients': [email_parser.extract_email_address(addr) for addr in email_parser.get_header_value('To').split(',')], 169 | 'subject': email_parser.get_header_value('Subject'), 170 | 'date': email_parser.get_header_value('Date'), 171 | 'content': processed_content 172 | } 173 | except requests.HTTPError as e: 174 | print(f"HTTPError while getting message data: {e}") 175 | raise 176 | except Exception as e: 177 | print(f"Error while getting message data: {e}") 178 | raise 179 | 180 | def format_email_information(self, sender, recipient, subject, date, content): 181 | try: 182 | formatted_content = f"From: {sender}\nTo: {recipient}\nSubject: {subject}\nDate: {date}\n\n{content}" 183 | return formatted_content 184 | except Exception as e: 185 | print(f"Error while formatting email information: {e}") 186 | raise 187 | 188 | def process_messages(self, messages): 189 | all_senders = set() 190 | all_recipients = set() 191 | all_subjects = set() 192 | contents = [] 193 | 194 | # Sort messages by date 195 | messages.sort(key=lambda msg: parse(self.get_message_data(msg[0])["date"]), reverse=True) 196 | 197 | most_recent_sender = None 198 | for idx, message_data in enumerate(messages): 199 | message_id, message_labels = message_data 200 | message_information = self.get_message_data(message_id) 201 | if message_information is None: 202 | continue # Skip this message and continue with the next one 203 | sender = message_information["sender"] 204 | recipients = message_information["recipients"] 205 | subject = message_information["subject"] 206 | all_senders.add(sender) 207 | all_recipients.update(recipients) 208 | all_subjects.add(subject) 209 | cleaned_content = message_information["content"] 210 | content_processor = ContentProcessor(cleaned_content) 211 | cleaned_content = content_processor.remove_signatures() 212 | cleaned_content = content_processor.normalize_content() 213 | formatted_message = self.format_email_information( 214 | sender, 215 | recipients, 216 | subject, 217 | message_information["date"], 218 | cleaned_content 219 | ) 220 | contents.append(formatted_message) 221 | 222 | # Update the most recent sender 223 | if idx == 0: # This is the most recent message 224 | most_recent_sender = sender 225 | 226 | return most_recent_sender, all_senders, all_recipients, all_subjects, contents 227 | 228 | def print_debug_info(self, message_id, thread_id, all_senders, all_recipients, all_subjects, contents): 229 | print("MESSAGE PROCESSING DEBUGGING INFO:") 230 | print(f"Message ID: {message_id}") 231 | print(f"Thread ID: {thread_id}") 232 | print(f"All Senders: {', '.join(all_senders)}") 233 | print(f"All Recipients: {', '.join(all_recipients)}") 234 | print(f"All Subjects: {', '.join(all_subjects)}") 235 | print(f"Contents: {contents}") 236 | 237 | class ContentProcessor: 238 | def __init__(self, content): 239 | self.content = content 240 | print("ContentProcessor initialized with content.") 241 | 242 | def remove_signatures(self): 243 | # Assuming signatures are separated by "--" or similar markers 244 | signature_pattern = re.compile(r'(--\s*\n.*$)', re.MULTILINE | re.DOTALL) 245 | self.content, _ = re.subn(signature_pattern, '', self.content) 246 | return self.content 247 | 248 | def normalize_content(self): 249 | # Normalize whitespace and line breaks 250 | self.content = re.sub(r'\r\n', '\n', self.content) # Normalize line breaks 251 | self.content = re.sub(r'[ \t]+', ' ', self.content) # Normalize spaces 252 | self.content = re.sub(r'\n{3,}', '\n\n', self.content) # Normalize multiple line breaks 253 | return self.content 254 | 255 | def extract_reply_body(self): 256 | # Use the EmailParser's method to remove quoted content 257 | # Since ContentProcessor does not handle headers, we should not attempt to extract headers here 258 | # Instead, pass the content directly to the EmailParser 259 | email_parser = EmailParser({'body': {'data': self.content}}) 260 | content_type = 'text/html' if '' in self.content.lower() else 'text/plain' 261 | self.content = email_parser.remove_quoted_content(self.content, content_type) 262 | return self.content 263 | 264 | 265 | def handler(pd: "pipedream"): 266 | try: 267 | # Step 1: Instantiate Classes 268 | token = f'{pd.inputs["gmail_custom_oauth"]["$auth"]["oauth_access_token"]}' 269 | authorization = f'Bearer {token}' 270 | headers = {"Authorization": authorization} 271 | gmail_api = GmailAPI(headers) 272 | 273 | # Step 2: Extract Email Details 274 | message_id = pd.steps["trigger"]["event"]["id"] 275 | thread_id = pd.steps["trigger"]["event"]["threadId"] 276 | payload = pd.steps["trigger"]["event"]["payload"] 277 | email_parser = EmailParser(payload) 278 | sender, recipients, subject, date = email_parser.extract_details() 279 | 280 | if None in (sender, recipients, subject, date): 281 | print("One or more essential details are missing from the email. Skipping this message.") 282 | return # Skip to the next message 283 | 284 | # Step 3: Fetch Thread Messages 285 | messages = gmail_api.get_thread_messages(thread_id) 286 | 287 | # Step 4: Process Each Message 288 | contents = [] 289 | for message_data in messages: 290 | message_id, _ = message_data 291 | message_info = gmail_api.get_message_data(message_id) 292 | if message_info is None: 293 | continue # Skip this message and continue with the next one 294 | content_processor = ContentProcessor(message_info["content"]) 295 | processed_content = content_processor.remove_signatures() 296 | processed_content = content_processor.normalize_content() 297 | reply_body = content_processor.extract_reply_body() 298 | formatted_message = gmail_api.format_email_information( 299 | sender, recipients, subject, date, reply_body 300 | ) 301 | contents.append(formatted_message) 302 | 303 | # Step 5: Compile Results 304 | most_recent_sender = sender if sender else recipients[0] 305 | 306 | # Step 6: Export Results 307 | pd.export("most_recent_sender", most_recent_sender) 308 | pd.export("message_id", message_id) 309 | pd.export("thread_id", thread_id) 310 | pd.export("sender", sender) 311 | pd.export("recipient", recipients[0] if recipients else None) 312 | pd.export("subject", subject) 313 | pd.export("contents", contents) 314 | 315 | except KeyError as error: 316 | print(f"An error occurred: {error}") 317 | return pd.flow.exit(f"Error: {error}") 318 | 319 | except (ValueError, requests.HTTPError) as error: 320 | print(f"An error occurred: {error}") 321 | return pd.flow.exit(f"Error: {error}") -------------------------------------------------------------------------------- /workflow/2_semantic_routers/entry.py: -------------------------------------------------------------------------------- 1 | # packages for interacting with OpenAI API and token counting 2 | from simpleaichat import AsyncAIChat, AIChat 3 | from pydantic import BaseModel, Field 4 | import asyncio 5 | import tiktoken 6 | # packages for interacting with Gmail API for edge case handling 7 | import requests 8 | import base64 9 | from email.message import EmailMessage 10 | 11 | 12 | # Classification Function Call and Ruleset. Solves for "from a real person vs spam" 13 | 14 | class EmailClassification(BaseModel): 15 | """ 16 | This class represents the classification of an email, determining whether it requires a response. 17 | """ 18 | requiresResponse: int = Field( 19 | description="Can either be 0 or 1. 0 if the email is a notification, a billing reminder, a social media update, spam, or some other sort of email that a person would not generally respond to. 1 if the email is from a real person with a genuine inquiry that requires some sort of response. Specific notification exceptions for [example@example.com], as [reasoning].", 20 | ge=0, le=1 21 | ) 22 | 23 | # Sales & Lead Gen Relevancy Function Call and Ruleset. Solves for "would an sdr/sales team member be interested in replying to this email?" 24 | 25 | class EmailRelevancy(BaseModel): 26 | """ 27 | This class represents the relevancy of an email to the Organization inbox. 28 | """ 29 | isRelevant: int = Field( 30 | description="Can either be 0, 1 or 2 . 0 if the email is not relevant to the Organization inbox, 1 if the email is relevant or 2 if the email is relevant but sensitive and requires stakeholder attention. The rulset for relevancy is any emails related to lead generation, service inquiries, sales call scheduling, inquries from current or potential clients or any sales and or Organizationanic inbound. Recruiting, inbound sales emails from other companies, or current client work or newsletters or literally anything not regarding prospecting is irrelevant. Emails that are relevant but sensitive would be any regarding pricing, contract negotiations, client complaints, or basically any email where an sdr would pass off the email chain to legal or high executives.", 31 | ge=0, le=1 32 | ) 33 | 34 | # Sensitivity Function Call and Ruleset. Solves for "ok this is from a real person and relevant to sales, but is this above my pay grade i.e does this require a stakeholder who has higher/broader context of this business than me as an sdr? " 35 | 36 | class EmailSensitivity(BaseModel): 37 | """ 38 | This class represents the sensitivity of an email to the Organization inbox. 39 | """ 40 | isSensitive: int = Field( 41 | description="Can either be 0 or 1. 0 if the email is not sensitive, 1 if the email is sensitive and requires stakeholder attention. Sensitive emails include any regarding pricing, requests for a contract, contract negotiations, client complaints, project scoping, or basically any email where an SDR would pass off the email chain to legal or high executives.", 42 | ge=0, le=1 43 | ) 44 | 45 | # Sentiment and Funnel Classification. Solves for "how does this person sound and at what point of the funnel is being reflected in the emails. Set to match deal funnel stages in crm to move leads along automatically but not irregularly." 46 | 47 | class EmailSentimentAndFunnelStage(BaseModel): 48 | """ 49 | This class represents the combined sentiment and funnel stage labeling of an email. 50 | """ 51 | label: str = Field( 52 | description="The label assigned to the email based on sentiment and funnel stage. Can be: not interested, interested, lead, appointment scheduled, qualified to buy, presentation scheduled, decision maker bought in, contract sent, closed won." 53 | ) 54 | 55 | # Thread Scenario and ICP Category Function Call and Ruleset. Solves for light attribution and pattern matching ICP based on content of thread. 56 | 57 | class EmailScenario(BaseModel): 58 | """ 59 | This class represents the scenario or context of the email, categorizing it based on the inquiry type, 60 | sender category, and specific inquiry details. 61 | """ 62 | inquiry_type: str = Field(description="Type of inquiry. Can include: Organizationanic inbound, cold email outbound reply, warm intro reply.") 63 | sender_category: str = Field(description="Category of sender. Can include: ICP 1: Silent Giant, ICP 2: High Growth Innovation, Other ICP Type.", default=None) 64 | 65 | 66 | class TokenCounter: 67 | def __init__(self, pd): 68 | self.pd = pd 69 | self.encoding = tiktoken.get_encoding("cl100k_base") 70 | 71 | def count_tokens(self, text): 72 | return len(self.encoding.encode(text)) 73 | 74 | def count_and_export_tokens(self, input_text, output_result, export_name): 75 | # Count tokens in the input and output 76 | input_token_count = self.count_tokens(input_text) 77 | output_token_count = self.count_tokens(str(output_result)) 78 | total_tokens = input_token_count + output_token_count 79 | 80 | # Export the total token count 81 | self.pd.export(export_name, total_tokens) 82 | 83 | # Return the total token count 84 | return total_tokens 85 | 86 | 87 | class EmailHandler: 88 | def __init__(self, pd, config, assembled_prompts): 89 | self.pd = pd 90 | self.config = config 91 | self.assembled_prompts = assembled_prompts 92 | self.token_counter = TokenCounter(pd) 93 | self.token = f'{pd.inputs["openai"]["$auth"]["api_key"]}' 94 | self.authorization = f'Bearer {self.token}' 95 | self.headers = {"Authorization": self.authorization} 96 | self.ai_sync = AIChat(api_key=self.token, console=False, model="gpt-4-0125-preview", headers=self.headers, temperature=0.0) 97 | self.ai_async = AsyncAIChat(api_key=self.token, console=False, model="gpt-4-0125-preview", headers=self.headers, temperature=0.0) 98 | self.sender = pd.steps["parse_thread"]["sender"] 99 | self.recipient = pd.steps["parse_thread"]["recipient"] 100 | self.subject = pd.steps["parse_thread"]["subject"] 101 | self.content = pd.steps["parse_thread"]["contents"] 102 | self.input_data = f"Sender: {self.sender}. Recipient: {self.recipient}. Subject: {self.subject}. Content: {self.content}." 103 | 104 | def classify_email(self): 105 | print("Classifying email...") 106 | result = self.ai_sync(self.input_data, system=self.assembled_prompts["email_classification"], output_schema=EmailClassification) 107 | print(f"Email classified. Result: {result}") 108 | self.token_counter.count_and_export_tokens(self.input_data, str(result), "classify_email_tokens") 109 | return result 110 | 111 | 112 | def check_relevancy(self): 113 | print("Checking relevancy...") 114 | result = self.ai_sync(self.input_data, system=self.assembled_prompts["email_relevancy"], output_schema=EmailRelevancy) 115 | print(f"Email Relevancy Result: {result}") 116 | self.token_counter.count_and_export_tokens(self.input_data, str(result), "check_relevancy_tokens") 117 | return result 118 | 119 | def check_sensitivity(self, classification_result, relevancy_result): 120 | # Generate the sensitivity prompt 121 | system_prompt_sensitivity = self.assembled_prompts["email_sensitivity"] 122 | print("Checking sensitivity...") 123 | self.sensitivity_result = self.ai_sync( 124 | self.input_data, system=system_prompt_sensitivity, output_schema=EmailSensitivity 125 | ) 126 | print(f"Email Sensitivity Result: {self.sensitivity_result}") 127 | self.token_counter.count_and_export_tokens( 128 | self.input_data, str(self.sensitivity_result), "check_sensitivity_tokens" 129 | ) 130 | return self.sensitivity_result 131 | 132 | def classify_sentiment_and_funnel_stage(self): 133 | # Now use the prompt to classify the sentiment and funnel stage 134 | result = self.ai_sync(self.input_data, system=self.assembled_prompts["email_sentiment_and_funnel_stage"], output_schema=EmailSentimentAndFunnelStage) 135 | print(f"Sentiment and funnel stage classified. Result: {result}") 136 | self.token_counter.count_and_export_tokens(self.input_data, str(result), "classify_sentiment_and_funnel_stage_tokens") 137 | return result 138 | 139 | def apply_label(self, label_category, label_name, thread_id): 140 | # Access the specific category from the email_labels configuration 141 | category_labels = self.config["email_labels"].get(label_category, {}) 142 | 143 | # Fetch the label ID using the label name. Since label_id is directly the string we need, no further .get("id") is required. 144 | label_id = category_labels.get(label_name.upper()) # Removed the erroneous .get("id") 145 | 146 | if label_id: 147 | apply_label_instance = ApplyLabel(self.pd, [label_id]) 148 | apply_label_instance.apply_labels(thread_id) 149 | else: 150 | print(f"Label ID for {label_name} in category {label_category} not found. Skipping label application.") 151 | 152 | def handle_async_tasks(self): 153 | # Lazy initialization of the scenario prompt 154 | system_prompt_scenario = self.assembled_prompts["email_scenario"] 155 | system_prompt_sentiment_and_funnel_stage = self.assembled_prompts["email_sentiment_and_funnel_stage"] 156 | async def async_function(): 157 | tasks = [ 158 | self.ai_async(self.input_data, system=system_prompt_scenario, output_schema=EmailScenario), 159 | self.ai_async(self.input_data, system=system_prompt_sentiment_and_funnel_stage, output_schema=EmailSentimentAndFunnelStage) 160 | ] 161 | results = await asyncio.gather(*tasks) 162 | 163 | # Count and export tokens for each task 164 | for i, result in enumerate(results): 165 | self.token_counter.count_and_export_tokens(self.input_data, str(result), f"handle_async_tasks_tokens_{i}") 166 | 167 | return results 168 | 169 | loop = asyncio.new_event_loop() 170 | asyncio.set_event_loop(loop) 171 | return loop.run_until_complete(async_function()) 172 | 173 | 174 | class ApplyLabel: 175 | def __init__(self, pd, label_ids): 176 | self.pd = pd 177 | self.label_ids = label_ids 178 | self.headers = { 179 | "Authorization": f'Bearer {self.pd.inputs["gmail_custom_oauth"]["$auth"]["oauth_access_token"]}', 180 | "Content-Type": "application/json" 181 | } 182 | 183 | def get_current_labels(self, thread_id): 184 | url = f'https://gmail.googleapis.com/gmail/v1/users/me/threads/{thread_id}' 185 | response = requests.get(url, headers=self.headers) 186 | if response.status_code == 200: 187 | thread_data = response.json() 188 | current_labels = [message['labelIds'] for message in thread_data['messages']] 189 | # Flatten the list of label IDs and remove duplicates 190 | return set([label for sublist in current_labels for label in sublist]) 191 | else: 192 | print(f"Error fetching current labels: {response.text}") 193 | raise Exception(f"Error fetching current labels: {response.text}") 194 | 195 | def apply_labels(self, thread_id): 196 | # Fetch current labels on the thread 197 | current_labels = self.get_current_labels(thread_id) 198 | print(f"current_labels: {current_labels}") 199 | 200 | # Filter out labels that already exist on the thread 201 | new_labels_to_add = [label_id for label_id in self.label_ids if label_id not in current_labels] 202 | 203 | # If there are no new labels to add, skip the API call 204 | if not new_labels_to_add: 205 | print("No new labels to add. Skipping label application.") 206 | return 207 | 208 | # Prepare the payload with the new labels to add 209 | payload = { 210 | "addLabelIds": new_labels_to_add 211 | } 212 | 213 | # Make the API call to modify the labels 214 | url = f'https://gmail.googleapis.com/gmail/v1/users/me/threads/{thread_id}/modify' 215 | response = requests.post(url, headers=self.headers, json=payload) 216 | if response.status_code == 200: 217 | print(f"Labels successfully applied to thread {thread_id}") 218 | else: 219 | print(f"Error applying labels: {response.text}") 220 | raise Exception(f"Error applying labels: {response.text}") 221 | 222 | 223 | def handler(pd: "pipedream"): 224 | # import config 225 | semantic_routers_config = pd.steps["workflow_config"]["semantic_routers_config"] 226 | email_labels = semantic_routers_config["email_labels"] 227 | print("semantic_router_config", semantic_routers_config) 228 | assembled_prompts = pd.steps["workflow_config"]["assembled_prompts"] 229 | #setup handler and token count 230 | email_handler = EmailHandler(pd, semantic_routers_config, assembled_prompts) 231 | total_tokens = 0 232 | 233 | # Perform the EmailClassification task and count tokens 234 | print("Starting Email Classification...") 235 | classification_result = email_handler.classify_email() 236 | pd.export("classification result:", classification_result) 237 | classify_email_tokens = email_handler.token_counter.count_and_export_tokens(email_handler.input_data, classification_result, "classify_email_tokens") 238 | total_tokens += classify_email_tokens 239 | 240 | # Dynamically determine the label for EmailClassification 241 | classification_label = "NOT_FROM_REAL_PERSON" if classification_result['requiresResponse'] == 0 else None 242 | if classification_label: 243 | print(f"Applying '{classification_label}' label and exiting workflow...") 244 | email_handler.apply_label("EmailClassification", classification_label, pd.steps["trigger"]["event"]["threadId"]) 245 | pd.export("total_tokens", total_tokens) 246 | return pd.flow.exit('Email does not require a response. Exiting workflow.') 247 | 248 | # Perform the EmailRelevancy task and count tokens 249 | print("Starting Email Relevancy Check...") 250 | relevancy_result = email_handler.check_relevancy() 251 | pd.export("relevancy result:", relevancy_result) 252 | relevancy_tokens = email_handler.token_counter.count_and_export_tokens(email_handler.input_data, relevancy_result, "check_relevancy_tokens") 253 | total_tokens += relevancy_tokens 254 | 255 | # Dynamically determine the label for EmailRelevancy 256 | relevancy_label = "NON_PROSPECTING_RELATED" if relevancy_result['isRelevant'] == 0 else None 257 | if relevancy_label: 258 | print(f"Applying '{relevancy_label}' label and exiting workflow...") 259 | email_handler.apply_label("EmailRelevancy", relevancy_label, pd.steps["trigger"]["event"]["threadId"]) 260 | pd.export("total_tokens", total_tokens) 261 | return pd.flow.exit('Email is not relevant. Exiting workflow.') 262 | 263 | # Perform the EmailSensitivity task using the results of the previous steps and count tokens 264 | print("Starting Email Sensitivity Check...") 265 | sensitivity_result = email_handler.check_sensitivity(classification_result, relevancy_result) 266 | pd.export("sensitivity result:", sensitivity_result) 267 | sensitivity_tokens = email_handler.token_counter.count_and_export_tokens(email_handler.input_data, sensitivity_result, "check_sensitivity_tokens") 268 | total_tokens += sensitivity_tokens # Accumulate total tokens 269 | 270 | # Conditional logic based on sensitivity result 271 | if sensitivity_result['isSensitive'] == 1: 272 | # If email is sensitive then export result, which will be used in sending to just send notification to relevant stakeholder rather than generate a reply 273 | print("Email is sensitive. Skipping further classification.") 274 | pd.export("total_tokens", total_tokens) # Export total tokens before exiting 275 | return 276 | else: 277 | # Proceed with asynchronous tasks for non-sensitive emails 278 | print("Starting Asynchronous Tasks for Scenario and Sentiment & Funnel Stage...") 279 | scenario_result, sentiment_and_funnel_stage_result = email_handler.handle_async_tasks() 280 | 281 | # Count tokens for scenario and sentiment/funnel stage classification 282 | scenario_tokens = email_handler.token_counter.count_and_export_tokens(email_handler.input_data, scenario_result, "scenario_tokens") 283 | sentiment_and_funnel_stage_tokens = email_handler.token_counter.count_and_export_tokens(email_handler.input_data, sentiment_and_funnel_stage_result, "sentiment_and_funnel_stage_tokens") 284 | total_tokens += scenario_tokens + sentiment_and_funnel_stage_tokens # Accumulate total tokens 285 | 286 | pd.export("Sentiment and Funnel Stage Result", sentiment_and_funnel_stage_result) 287 | pd.export("email_scenario_result", scenario_result) 288 | 289 | 290 | # Apply labels based on the scenario and sentiment/funnel stage results 291 | print("Applying labels based on Scenario and Sentiment & Funnel Stage results...") 292 | if scenario_result['inquiry_type']: 293 | email_handler.apply_label("EmailScenario", scenario_result['inquiry_type'], pd.steps["trigger"]["event"]["threadId"]) 294 | if scenario_result['sender_category']: 295 | email_handler.apply_label("EmailScenario", scenario_result['sender_category'], pd.steps["trigger"]["event"]["threadId"]) 296 | if sentiment_and_funnel_stage_result['label']: 297 | email_handler.apply_label("EmailSentimentAndFunnelStage", sentiment_and_funnel_stage_result['label'], pd.steps["trigger"]["event"]["threadId"]) 298 | 299 | # Sum the total tokens used in each step 300 | print(f"Total Tokens Used: {total_tokens}") 301 | 302 | # Export the results for use in the next step 303 | pd.export("total_tokens", total_tokens) 304 | -------------------------------------------------------------------------------- /workflow/3_reply_drafter_and_assembler/entry.py: -------------------------------------------------------------------------------- 1 | # packages for interacting with Gmail API 2 | import base64 3 | from email.message import EmailMessage 4 | from email.headerregistry import Address 5 | from email.utils import make_msgid 6 | # packages for interacting with OpenAI API 7 | from simpleaichat import AIChat 8 | import tiktoken 9 | import markdown 10 | 11 | 12 | # Drafting setup prompt. Will produce high quality context-aware responses that are succinct and use links to case studies and other resources 13 | common_context = """ 14 | # Cold Email & Reply Drafting Prompt 15 | 16 | You are monitoring [ORGANIZATION]'s primary sales, lead generation and general inbound email account. You have one task as a part of a larger system 17 | which will be described below. For context the larger system designed to provide an automated, intelligent solution for managing emails at the [ORGANIZATION]. It is comprised of: 18 | 19 | 1. Email Classification (is this email from a real person or a notification/spam?) 20 | 2. Email Scenario Identification (what is the nature of the email? Who is it from? What is it about?) 21 | 3. Funnel Stage Classification (what stage of the sales funnel does this email seem like?) 22 | 4. Stakeholder Notification (if the email is from a real person, is there a member of [ORGANIZATION] is best suited to address the inquiry or not?) 23 | 5. Contextually Aware & Scenario Specific Generative AI drafting and reply (Using the email's classification, scenario, and stakeholder notification, along with specific instructions for each scenario and a general tone of voice and SOPs generate a contextually aware and scenario specific reply to the email) 24 | 25 | YOUR ACTIONS MUST STRICTLY FOLLOW THE PROVIDED RULESETS IN EACH FUNCTION. IF YOU GENERATE ANYTHING OUTSIDE OF THE RULESET, YOU FAIL THE TEST. 26 | 27 | You are responsible for the below task: 28 | 29 | You are handling step 5. You are to act as the intelligent email drafting AI assistant for [ORGANIZATION]. You are tasked with drafting a contextually aware and scenario specific replies to emails. You are drafting replies for [NAME], the founder of [ORGANIZATION] to review and send himself, keep this in mind. 30 | 31 | Make sure the email is personalized to the sender, addresses content relevantly and succinctly.Adopt the tone of voice and persona: 32 | 33 | Balance excitment with professionalism. Messages that only necessitate a sort reply should have a short reply. Never be overly verbose or fluffy in your replies, almost spartan. Above, all limit corny phrases and euphemisms. 34 | 35 | Your signature will be included, YOU DO NOT NEED TO WRITE YOUR SIGNATURE. NEVER INCLUDE A SIGNATURE. IF YOU INCLUDE A SIGNATURE YOU FAIL THE TEST. 36 | 37 | NEVER PRINT THE EMAIL OR SUBJECT IN YOUR RESPONSE. ONLY PRINT THE RELEVANT CONTENT AS IF YOU WERE RESPONDING TO THE EMAIL. 38 | 39 | Your responses should be succinct. Your primary objective is to write a response to the message that addresses their primary inquiry, and anticipates follow-up questions and addresses them, in as few sentences as possible. Think of it is a game - "how can I use as few words as possible to write the best, most actionable information-dense helpful response possible?" (hint: links to case studies can be a good way to optimize density) is how you should approach the problem. 40 | 41 | When someone inquires with a question about a case study, scheduling a call/meeting with someone at [ORGANIZATION], you should provide a link to the Hubspot scheduling link, using the below mentioned placeholder, for that person or the case study that most cleary demonstrates [ORGANIZATION]'s ability to address their problem and create value specific to them. 42 | 43 | When referring to links, only use "here" for scheduling links. YOU MUST FOLLOW MARKDOWN FORMATTING FOR REFERENCING CASE STUDIES. NEVER USE "here" for case study links. When you reference a case study you must used a [Case Study] placeholder. 44 | 45 | When referencing a case study or other link, you should use the designated placeholder signified by the [case study] or [here] for the hubspot scheduling link (exact match, always all proper capitalization case) placeholders. For example if you want to reference Helix, you would write [Helix]. 46 | 47 | 48 | Outline: 49 | - Intro (Company, Tone of voice, Brand Proposition) 50 | - Case studies 51 | - Case Studies by discipline 52 | - Exhaustive case study list 53 | - Ideal Customer Profiles 54 | - ICP Messaging 55 | - Case Study ICP Pairs 56 | - Services 57 | - ICP Services Pairs 58 | 59 | ## Intro 60 | 61 | """ 62 | 63 | 64 | class EmailAssembler: 65 | def __init__(self, response, original_email, drafting_config): 66 | self.response = response 67 | self.original_email = original_email 68 | self.variable_context = drafting_config["variable_context"] 69 | self.signature_block = drafting_config["signature_block"] 70 | self.links_for_insertion = drafting_config["links_for_insertion"] 71 | self.formatted_message = self.format_original_message(original_email['content']) 72 | self.response = self.inject_links() # Call inject_links here 73 | self.context_block = self.create_context_block() 74 | 75 | def format_original_message(self, original_message): 76 | # Join the list of strings into a single string 77 | original_message = ' '.join(original_message) 78 | lines = original_message.split('\n') 79 | headers = lines[:4] 80 | content = lines[4:] 81 | formatted_headers = [f"> {line} " for line in headers if line.strip() != ''] 82 | formatted_content = [f"> {line} " for line in content if line.strip() != ''] 83 | return '\n'.join(formatted_headers + [''] + formatted_content) 84 | 85 | def create_context_block(self): 86 | # Use the signature_block from the config 87 | context_block = f""" 88 | {self.response} 89 | 90 | {self.signature_block} 91 | """ 92 | return '\n'.join(line.lstrip() for line in context_block.split('\n')) 93 | 94 | def inject_links(self) -> str: 95 | # Replace the placeholders with the actual links from the config 96 | for placeholder, link in self.links_for_insertion.items(): 97 | markdown_link = f"[{placeholder.strip('[]')}]({link})" 98 | self.response = self.response.replace(placeholder, markdown_link) 99 | return self.response 100 | 101 | def assemble_email(self): 102 | # Convert markdown to HTML for each part of the context block 103 | parts = self.context_block.split('---') 104 | html_parts = [markdown.markdown(part) for part in parts] 105 | return '---'.join(html_parts) 106 | 107 | class TokenCounter: 108 | def __init__(self, pd): 109 | self.pd = pd 110 | self.encoding = tiktoken.get_encoding("cl100k_base") 111 | 112 | def count_tokens(self, text): 113 | return len(self.encoding.encode(text)) 114 | 115 | def count_and_export_tokens(self, input_text, output_result, export_name): 116 | # Count tokens in the input and output 117 | input_token_count = self.count_tokens(input_text) 118 | output_token_count = self.count_tokens(str(output_result)) 119 | total_tokens = input_token_count + output_token_count 120 | 121 | # Export the total token count 122 | self.pd.export(export_name, total_tokens) 123 | 124 | # Return the total token count 125 | return total_tokens 126 | 127 | def handler(pd: "pipedream"): 128 | try: 129 | sensitivity_result = pd.steps["semantic_routers"]["sensitivity result:"]["isSensitive"] 130 | 131 | variable_context = pd.steps["workflow_config"]["drafting_config"]["variable_context"] 132 | 133 | # Check if 'requiresResponse' is 1 and set the forwarding instruction 134 | if sensitivity_result == 1: 135 | # Set the forwarding instruction 136 | forwarding_instruction = "Please forward this email to stakeholder@example.com for further review." 137 | pd.export("forwarding_instruction", forwarding_instruction) 138 | print("Forwarding instruction set. Exiting handler.") 139 | return # Exit the handler early 140 | else: 141 | # Instantiate the TokenCounter 142 | token_counter = TokenCounter(pd) 143 | # Extract original email details 144 | sender = pd.steps["parse_thread"]["sender"] 145 | recipient = pd.steps["parse_thread"]["recipient"] 146 | subject = pd.steps["parse_thread"]["subject"] 147 | content = pd.steps["parse_thread"]["contents"] 148 | 149 | print(f"Original email details - Sender: {sender}, Recipient: {recipient}, Subject: {subject}, Content: {content}") 150 | 151 | # Extract scenario details 152 | inquiry_type = pd.steps["semantic_routers"]["email_scenario_result"] 153 | 154 | full_drafting_context = common_context + variable_context 155 | 156 | # Determine system prompt based on inquiry type 157 | system_prompt = full_drafting_context + "ALWAYS GENERATE YOUR RESPONSE IN MARKDOWN. Please use markdown." 158 | 159 | # Generate response using simpleaichat 160 | token = f'{pd.inputs["openai"]["$auth"]["api_key"]}' 161 | authorization = f'Bearer {token}' 162 | headers = {"Authorization": authorization} 163 | ai = AIChat(console=False, save_messages=False, model="gpt-4-0125-preview", headers=headers, api_key=token, temperature=0.0) 164 | input_data = f"Subject: {subject}. Content: {content}. Inquiry Type: {inquiry_type}." 165 | print("Input data for AIChat: ", input_data) 166 | # Generate response using simpleaichat 167 | output = ai(input_data, system=system_prompt) 168 | print("Output from AIChat: ", output) 169 | 170 | # Count and export tokens for draft response 171 | draft_response_tokens = token_counter.count_and_export_tokens(input_data, output, "draft_response_tokens") 172 | 173 | # Assemble the email using EmailAssembler Class 174 | assembler = EmailAssembler(output, {"sender": sender, "recipient": recipient, "subject": subject, "content": content}, pd.steps["workflow_config"]["drafting_config"]) 175 | context_block = assembler.assemble_email() 176 | print("Context Block Email: ", context_block) 177 | 178 | total_tokens = draft_response_tokens 179 | 180 | # Export the results for visibility 181 | pd.export("Context Block Email: ", context_block) 182 | pd.export("draft_response_tokens", draft_response_tokens) 183 | pd.export("total tokens", total_tokens) 184 | 185 | except Exception as e: 186 | print(f"An error occurred: {str(e)}") 187 | pd.export("error", str(e)) 188 | -------------------------------------------------------------------------------- /workflow/4_sending_manager/entry.py: -------------------------------------------------------------------------------- 1 | from email.message import EmailMessage 2 | import base64 3 | import requests 4 | from email.parser import BytesParser 5 | 6 | 7 | class EmailManager: 8 | def __init__(self, pd, sending_manager_config): 9 | print("Initializing EmailManager...") 10 | self.pd = pd 11 | self.sending_email_address = sending_manager_config["sending_email_address"] 12 | self.whitelisted_domains = sending_manager_config["whitelisted_domains"] 13 | self.draft_or_autosend = sending_manager_config["draft_or_autosend"] 14 | self.headers = { 15 | "Authorization": f'Bearer {pd.inputs["gmail_custom_oauth"]["$auth"]["oauth_access_token"]}', 16 | "Content-Type": "application/json" 17 | } 18 | 19 | def is_domain_whitelisted(self, email_address): 20 | if self.whitelisted_domains["enabled"]: 21 | domain = email_address.split('@')[-1].lower() 22 | return domain in self.whitelisted_domains["domains"] 23 | return True # If whitelisting is disabled, allow all domains 24 | 25 | def handle_email(self, to, subject, content, thread_id, message_id): 26 | if self.draft_or_autosend == 0: 27 | # Create draft logic 28 | self.create_draft_existing_thread(to, subject, content, thread_id, message_id) 29 | else: 30 | # Send email logic 31 | self.send_email(to, subject, content, thread_id, message_id) 32 | 33 | def create_draft_existing_thread(self, to, subject, content, thread_id, message_id): 34 | # Construct the email message 35 | message = EmailMessage() 36 | message.set_content(content, subtype="html") 37 | message["To"] = to 38 | message["From"] = self.sending_email_address 39 | message["Subject"] = subject 40 | message["In-Reply-To"] = message_id 41 | message["References"] = message_id 42 | raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode("utf-8") 43 | 44 | # Create the draft with the correct headers for threading 45 | url = 'https://gmail.googleapis.com/gmail/v1/users/me/drafts' 46 | payload = { 47 | "message": { 48 | "raw": raw_message, 49 | "threadId": thread_id, 50 | "headers": [ 51 | {"name": "In-Reply-To", "value": message_id}, 52 | {"name": "References", "value": message_id} 53 | ] 54 | } 55 | } 56 | response = requests.post(url, headers=self.headers, json=payload) 57 | print(f"create_draft_existing_thread() response: {response.json()}") 58 | return response 59 | 60 | def send_email(self, to, subject, content, thread_id, message_id): 61 | print("Preparing to send email...") 62 | message = EmailMessage() 63 | message.set_content(content, subtype="html") 64 | message["To"] = to 65 | message["From"] = self.sending_email_address 66 | message["Subject"] = subject 67 | message["In-Reply-To"] = message_id 68 | message["References"] = message_id 69 | raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode("utf-8") 70 | self._send(raw_message, thread_id) 71 | 72 | def get_original_email(self, message_id): 73 | print("Retrieving original email...") 74 | url = f"https://gmail.googleapis.com/gmail/v1/users/me/messages/{message_id}?format=raw" 75 | response = requests.get(url, headers=self.headers) 76 | print(f"get_original_email response: {response}") 77 | if response.status_code != 200: 78 | raise Exception(f"Error occurred while retrieving the email. Status code: {response.status_code}") 79 | return response.json()['raw'] 80 | 81 | def forward_email(self, message_id, to_email): 82 | print("Forwarding email...") 83 | raw_message = self.get_original_email(message_id) 84 | decoded_message = base64.urlsafe_b64decode(raw_message.encode('ASCII')) 85 | 86 | # Parse the MIME message 87 | email_message = BytesParser().parsebytes(decoded_message) 88 | 89 | # Initialize an empty string to hold the plain text parts 90 | plain_text_content = "" 91 | 92 | # Iterate over all parts of the email 93 | for part in email_message.walk(): 94 | # Check if the part is a text part and is plain text 95 | if part.get_content_maintype() == 'text' and part.get_content_subtype() == 'plain': 96 | # Decode the part's payload 97 | payload = part.get_payload(decode=True) 98 | charset = part.get_content_charset() or 'utf-8' 99 | text = payload.decode(charset) 100 | plain_text_content += text + "\n\n" 101 | 102 | # Create the full content with the forwarding message 103 | notification = "A review of the original thread is required for full context and any possible attachments.\n\n" 104 | forwarding_message = f"---------- Forwarded message ---------\nFrom: {email_message['From']}\nDate: {email_message['Date']}\nSubject: {email_message['Subject']}\n\n" 105 | full_content = notification + forwarding_message + plain_text_content.strip() 106 | 107 | # Create a new EmailMessage object for the forwarded email 108 | forwarded_email = EmailMessage() 109 | forwarded_email.set_content(full_content) 110 | forwarded_email['To'] = to_email 111 | forwarded_email['From'] = self.sending_email_address 112 | forwarded_email['Subject'] = f"Fwd: {email_message['Subject']}" 113 | 114 | # Re-encode the modified email 115 | raw_forwarded_message = base64.urlsafe_b64encode(forwarded_email.as_bytes()).decode('utf-8') 116 | 117 | # Send the modified email 118 | self._send(raw_forwarded_message, email_message['threadId']) 119 | 120 | def _send(self, raw_message, thread_id): 121 | print("Sending email...") 122 | url = 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send' 123 | payload = {"raw": raw_message, "threadId": thread_id} 124 | response = requests.post(url, headers=self.headers, json=payload) 125 | if response.status_code != 200: 126 | raise Exception(f"Error occurred while sending the email. Status code: {response.status_code}") 127 | print(f"Email sent. Status code: {response.status_code}") 128 | self.pd.export("email_send_status_code", response.status_code) 129 | 130 | # Existing function to retrieve the IMAP message ID 131 | def get_imap_message_id(headers, latest_message_id): 132 | print("Retrieving IMAP message ID...") 133 | url = f"https://gmail.googleapis.com/gmail/v1/users/me/messages/{latest_message_id}?format=metadata&metadataHeaders=message-id" 134 | response = requests.get(url, headers=headers).json() 135 | print(f"get_imap_message_id() response: {response}") # Log the response 136 | if "payload" not in response: 137 | raise Exception("Error: 'payload' key not found in the response") 138 | imap_message_id = "" 139 | for header in response["payload"]["headers"]: 140 | if header["name"] == "Message-ID": 141 | imap_message_id = header["value"] 142 | return imap_message_id 143 | 144 | def handler(pd: "pipedream"): 145 | print("Starting handler...") 146 | sending_manager_config = pd.steps["workflow_config"]["sending_manager_config"] 147 | email_manager = EmailManager(pd, sending_manager_config) 148 | 149 | try: 150 | # Define a list of whitelisted domains. If the list is empty, all domains are allowed. 151 | whitelisted_domains = ["makebttr.com", "jdietle.me"] 152 | 153 | sensitivity_result = pd.steps["semantic_routers"]["sensitivity result:"]["isSensitive"] 154 | print(f"Sensitivity result: {sensitivity_result}") 155 | subject = pd.steps["parse_thread"]["subject"] 156 | thread_id = pd.steps["parse_thread"]["thread_id"] 157 | most_recent_sender = pd.steps["parse_thread"]["most_recent_sender"] 158 | 159 | # Perform domain check only if whitelisted domains are defined 160 | if whitelisted_domains: 161 | print("Checking recipient domain...") 162 | recipient_domain = most_recent_sender.split('@')[-1].lower() 163 | if recipient_domain not in whitelisted_domains: 164 | print(f"Recipient domain '{recipient_domain}' is not allowed. Exiting workflow.") 165 | pd.export("email_status", f"Recipient domain '{recipient_domain}' is not allowed.") 166 | return # Exit the handler early 167 | 168 | if sensitivity_result == 1: 169 | # Forward the original email to the stakeholder 170 | print("Forwarding original email to stakeholder...") 171 | original_message_id = pd.steps["parse_thread"]["message_id"] 172 | email_manager.forward_email(original_message_id, "jacob@jdietle.me") 173 | return pd.flow.exit("Sensitive Thread. Skipping Hubspot & Forwarding") 174 | else: 175 | print("Checking draft or autosend setting...") 176 | if sending_manager_config["draft_or_autosend"] == 0: 177 | print("Drafting email...") 178 | context_block = pd.steps["reply_drafter_and_assembler"]["Context Block Email: "] 179 | if isinstance(context_block, list): 180 | context_block = '\n'.join(context_block) 181 | message_id = get_imap_message_id(email_manager.headers, pd.steps["parse_thread"]["message_id"]) 182 | email_manager.create_draft_existing_thread(most_recent_sender, subject, context_block, thread_id, message_id) 183 | else: 184 | print("Sending email...") 185 | context_block = pd.steps["reply_drafter_and_assembler"]["Context Block Email: "] 186 | if isinstance(context_block, list): 187 | context_block = '\n'.join(context_block) 188 | message_id = get_imap_message_id(email_manager.headers, pd.steps["parse_thread"]["message_id"]) 189 | email_manager.send_email(most_recent_sender, subject, context_block, thread_id, message_id) 190 | except Exception as e: 191 | print(f"An error occurred: {str(e)}") 192 | pd.export("email_status", f"Error: {str(e)}") 193 | -------------------------------------------------------------------------------- /workflow/5_hubspot_crm_update/entry.py: -------------------------------------------------------------------------------- 1 | # pipedream add-package simplejson 2 | # pipedream add-package delorean 3 | # pipedream add-package hubspot-api-client 4 | 5 | from hubspot.crm.contacts import SimplePublicObjectInput, ApiException as ContactsApiException 6 | from hubspot.crm.deals import ApiException as DealsApiException, PublicObjectSearchRequest, SimplePublicObjectInputForCreate, FilterGroup, Filter 7 | from hubspot.crm.timeline import ApiException as TimelineApiException 8 | import hubspot 9 | 10 | class HubSpotManager: 11 | def __init__(self, pd): 12 | self.pd = pd 13 | self.access_token = pd.inputs["hubspot_developer_app"]["$auth"]["oauth_access_token"] 14 | print(self.access_token) 15 | self.client = hubspot.Client.create(access_token=self.access_token) 16 | 17 | def get_contact_by_email(self, email): 18 | print(f"Attempting to get contact by email: {email}") 19 | try: 20 | search_request = { 21 | "filterGroups": [{ 22 | "filters": [{ 23 | "propertyName": "email", 24 | "operator": "EQ", 25 | "value": email 26 | }] 27 | }], 28 | "properties": ["email"], 29 | "limit": 1 30 | } 31 | response = self.client.crm.contacts.search_api.do_search(search_request) 32 | contacts = response.results 33 | if contacts: 34 | return contacts[0].id 35 | else: 36 | return None 37 | except ContactsApiException as e: 38 | print(f"Exception when searching for contact by email: {e}") 39 | return None 40 | 41 | def get_deal_by_contact_id(self, contact_id): 42 | print(f"Attempting to get deal by contact ID: {contact_id}") 43 | try: 44 | # Use the pseudo-property "associations.contact" to search for associated deals 45 | filter = Filter( 46 | property_name="associations.contact", 47 | operator="EQ", 48 | value=contact_id 49 | ) 50 | filter_group = FilterGroup(filters=[filter]) 51 | search_request = PublicObjectSearchRequest(filter_groups=[filter_group]) 52 | 53 | # Execute the search request 54 | api_response = self.client.crm.deals.search_api.do_search(public_object_search_request=search_request) 55 | 56 | # Check if there are any associated deals 57 | if api_response.total > 0: 58 | # Return the first associated deal ID 59 | return api_response.results[0].id 60 | else: 61 | return None 62 | except ApiException as e: 63 | print(f"Exception when searching for associated deals: {e}") 64 | return None 65 | 66 | def create_deal(self, contact_id, deal_properties): 67 | print("Attempting to create a new deal with properties:", deal_properties) 68 | try: 69 | # Define the association to the contact 70 | associations = { 71 | "associations": [ 72 | { 73 | "to": {"id": contact_id}, 74 | "types": [ 75 | { 76 | "associationCategory": "HUBSPOT_DEFINED", 77 | "associationTypeId": "3" 78 | } 79 | ] 80 | } 81 | ] 82 | } 83 | # Include the associations in the deal creation request 84 | simple_public_object_input_for_create = SimplePublicObjectInputForCreate( 85 | properties=deal_properties, 86 | **associations 87 | ) 88 | api_response = self.client.crm.deals.basic_api.create( 89 | simple_public_object_input_for_create=simple_public_object_input_for_create 90 | ) 91 | print(f"Deal created with ID: {api_response.id}") 92 | return api_response.id 93 | except DealsApiException as e: 94 | print(f"Exception when creating a new deal: {e}") 95 | return None 96 | 97 | def update_deal_stage(self, deal_id, stage_id): 98 | print(f"Attempting to update deal {deal_id} to stage {stage_id}") 99 | try: 100 | simple_public_object_input = SimplePublicObjectInput(properties={"dealstage": stage_id}) 101 | self.client.crm.deals.basic_api.update(deal_id=deal_id, simple_public_object_input=simple_public_object_input) 102 | print(f"Deal stage updated for deal ID {deal_id}") 103 | except DealsApiException as e: 104 | print(f"Exception when updating deal stage: {e}") 105 | 106 | def create_timeline_event(self, contact_id, event_template_id, event_properties): 107 | print(f"Attempting to create timeline event for contact ID {contact_id}") 108 | try: 109 | timeline_event = { 110 | "eventTemplateId": event_template_id, 111 | "email": contact_id, 112 | "tokens": event_properties 113 | } 114 | self.client.crm.timeline.events_api.create(timeline_event=timeline_event) 115 | print(f"Timeline event created for contact ID {contact_id}") 116 | except TimelineApiException as e: 117 | print(f"Exception when creating timeline event: {e}") 118 | 119 | 120 | 121 | def handler(pd: "pipedream"): 122 | print("Handler started") 123 | hubspot_manager = HubSpotManager(pd) 124 | email = pd.steps["parse_thread"]["most_recent_sender"] 125 | sentiment_and_funnel_stage_result = pd.steps["parallel_function_call_sentiment_analysis"]["Sentiment and Funnel Stage Result"]["label"] 126 | 127 | sentiment_and_funnel_stage_result = "appointment scheduled" 128 | 129 | print(f"sentiment_and_funnel_stage_result: {sentiment_and_funnel_stage_result}") 130 | 131 | # Map sentiment and funnel stage labels to HubSpot deal stages 132 | stage_mapping = { 133 | "appointment scheduled": "appointmentscheduled", 134 | "qualified to buy": "qualifiedtobuy", 135 | "presentation scheduled": "presentationscheduled", 136 | "decision maker bought in": "decisionmakerboughtin", 137 | "closed won": "closedwon", 138 | "closed lost": "closedlost" 139 | } 140 | 141 | # Check if the sentiment and funnel stage result matches a stage in the mapping 142 | if sentiment_and_funnel_stage_result.lower() in stage_mapping: 143 | # Get the HubSpot internal stage ID from the mapping 144 | stage_id = stage_mapping[sentiment_and_funnel_stage_result.lower()] 145 | 146 | # Get contact ID by email 147 | contact_id = hubspot_manager.get_contact_by_email(email) 148 | 149 | if contact_id: 150 | print(f"Contact ID {contact_id} found, proceeding with deal checks") 151 | # Check if a deal exists for the contact and update or create as necessary 152 | deal_id = hubspot_manager.get_deal_by_contact_id(contact_id) 153 | if deal_id: 154 | # Update the existing deal stage 155 | hubspot_manager.update_deal_stage(deal_id, stage_id) 156 | else: 157 | # Create a new deal if the stage is 'appointment scheduled' 158 | if sentiment_and_funnel_stage_result.lower() == 'appointment scheduled': 159 | deal_properties = { 160 | "dealname": "New Deal from Email", 161 | "dealstage": stage_id, 162 | "pipeline": "default" 163 | } 164 | # Create a new deal and associate it with the contact 165 | deal_id = hubspot_manager.create_deal(contact_id, deal_properties) 166 | else: 167 | print("No contact ID found, exiting handler") 168 | else: 169 | print(f"The sentiment and funnel stage result '{sentiment_and_funnel_stage_result}' does not match any deal stage.") 170 | -------------------------------------------------------------------------------- /workflow/label_setup_script___disable_after_deployment/entry.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | 4 | 5 | def create_label(pd, label_name, parent_label_id=None): 6 | headers_gmail = { 7 | "Authorization": f'Bearer {pd.inputs["gmail_custom_oauth"]["$auth"]["oauth_access_token"]}', 8 | "Content-Type": "application/json" 9 | } 10 | payload_create_label = { 11 | "name": label_name, 12 | "labelListVisibility": "labelShow", 13 | "messageListVisibility": "show", 14 | "parent": parent_label_id 15 | } 16 | url_gmail = 'https://gmail.googleapis.com/gmail/v1/users/me/labels' 17 | create_response = requests.post(url_gmail, headers=headers_gmail, data=json.dumps(payload_create_label)) 18 | if create_response.status_code == 200: 19 | label_info = create_response.json() 20 | print(f'Created label with name "{label_name}" and ID "{label_info.get('id')}".') 21 | return label_info 22 | else: 23 | print(f'An error occurred while creating the label "{label_name}".') 24 | print(f'Status code: {create_response.status_code}') 25 | print(f'Error message: {create_response.text}') 26 | return None 27 | 28 | def create_parent_labels(pd, label_categories): 29 | parent_labels = {} 30 | for category in label_categories: 31 | parent_label_info = create_label(pd, category) 32 | if parent_label_info: 33 | parent_labels[category] = parent_label_info['id'] 34 | else: 35 | print(f"Failed to create parent label for category '{category}'") 36 | return parent_labels 37 | 38 | def create_and_organize_labels(pd, label_categories, parent_labels): 39 | organized_labels = {category: {} for category in label_categories} 40 | 41 | for category, sub_labels in label_categories.items(): 42 | parent_label_id = parent_labels.get(category) 43 | for sub_label_name in sub_labels: 44 | label_info = create_label(pd, sub_label_name, parent_label_id) 45 | if label_info: 46 | organized_labels[category][sub_label_name] = label_info['id'] 47 | else: 48 | print(f"Failed to create sub label '{sub_label_name}' under parent label '{category}'") 49 | 50 | return organized_labels 51 | 52 | def handler(pd: "pipedream"): 53 | label_categories = { 54 | "EmailClassification": ["FROM_REAL_PERSON"], 55 | "EmailRelevancy": ["NON_PROSPECTING_RELATED"], 56 | "EmailSentimentAndFunnelStage": ["LEAD", "INTERESTED", "QUALIFIED_TO_BUY", "APPOINTMENT_SCHEDULED", "PRESENTATION_SCHEDULED", "DECISION_MAKER_BOUGHT_IN", "CONTRACT_SENT", "CLOSED_WON"], 57 | "EmailScenarioInquiryType": {"COLD_OUTBOUND_REPLY", "WARM_INTRO_REPLY", "ORGANIC_INBOUND"}, 58 | "EmailScenarioSenderCategory": {"ICP 1: Large Commercial Real Estate Firms", "ICP 2: Property Management Companies", "ICP_OTHER"} 59 | } 60 | 61 | # Create parent labels 62 | parent_labels = create_parent_labels(pd, label_categories) 63 | 64 | # Create sub labels and organize them 65 | organized_labels = create_and_organize_labels(pd, label_categories, parent_labels) 66 | 67 | # Export the organized labels as a JSON object 68 | pd.export("organized_labels", organized_labels) 69 | 70 | -------------------------------------------------------------------------------- /workflow/workflow.yaml: -------------------------------------------------------------------------------- 1 | schema: workflow/2022.04 2 | name: 'WF 1A: Inbox Management & Auto-draft' 3 | settings: 4 | auto_retry: true 5 | error_notification: true 6 | lambda_memory: 1200 7 | lambda_timeout: 90 8 | triggers: 9 | - id: dc_WbuJmmq 10 | steps: 11 | - namespace: parse_thread 12 | runtime: python3.12 13 | uses: "./parse_thread/entry.py" 14 | props: 15 | __configurableProps: 16 | - app: python 17 | name: python 18 | type: app 19 | authType: 20 | appId: app_m9zhdv 21 | - app: gmail_custom_oauth 22 | name: gmail_custom_oauth 23 | type: app 24 | authType: oauth 25 | appId: app_13GhvW 26 | gmail_custom_oauth: 27 | authProvisionId: apn_QPhrPyz 28 | - namespace: semantic_routers 29 | runtime: python3.12 30 | uses: "./semantic_routers/entry.py" 31 | props: 32 | __configurableProps: 33 | - app: openai 34 | name: openai 35 | type: app 36 | authType: keys 37 | appId: app_mWnhBo 38 | - app: gmail_custom_oauth 39 | name: gmail_custom_oauth 40 | type: app 41 | authType: oauth 42 | appId: app_13GhvW 43 | openai: 44 | authProvisionId: apn_V1hpAPQ 45 | gmail_custom_oauth: 46 | authProvisionId: apn_KAh4JMj 47 | - namespace: reply_drafter_and_assembler 48 | runtime: python3.12 49 | uses: "./reply_drafter_and_assembler/entry.py" 50 | props: 51 | __configurableProps: 52 | - app: openai 53 | name: openai 54 | type: app 55 | authType: keys 56 | appId: app_mWnhBo 57 | openai: 58 | authProvisionId: apn_V1hpAPQ 59 | - namespace: sending_manager 60 | runtime: python3.12 61 | uses: "./sending_manager/entry.py" 62 | props: 63 | __configurableProps: 64 | - app: gmail_custom_oauth 65 | name: gmail_custom_oauth 66 | type: app 67 | authType: oauth 68 | appId: app_13GhvW 69 | - namespace: hubspot_crm_update 70 | runtime: python3.12 71 | uses: "./hubspot_crm_update/entry.py" 72 | props: 73 | __configurableProps: 74 | - app: hubspot_developer_app 75 | name: hubspot_developer_app 76 | type: app 77 | authType: oauth 78 | appId: app_mjEhNl 79 | - namespace: label_setup_script___disable_after_deployment 80 | disabled: true 81 | runtime: python3.12 82 | uses: "./label_setup_script___disable_after_deployment/entry.py" 83 | props: 84 | __configurableProps: 85 | - app: gmail_custom_oauth 86 | name: gmail_custom_oauth 87 | type: app 88 | authType: oauth 89 | appId: app_13GhvW 90 | 91 | --------------------------------------------------------------------------------