provekit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (485) hide show
  1. provekit/__init__.py +0 -0
  2. provekit/cli.py +256 -0
  3. provekit/config.py +77 -0
  4. provekit/database.py +132 -0
  5. provekit/main.py +115 -0
  6. provekit/migrations/env.py +40 -0
  7. provekit/migrations/script.py.mako +22 -0
  8. provekit/migrations/versions/323eb73d463c_user_email_verified.py +32 -0
  9. provekit/migrations/versions/49e8ab812556_baseline_schema.py +241 -0
  10. provekit/migrations/versions/819ed5ff183e_workspace_ingest_key.py +35 -0
  11. provekit/migrations/versions/a1b2c3d4e5f6_user_token_version.py +27 -0
  12. provekit/models.py +191 -0
  13. provekit/observability.py +154 -0
  14. provekit/routers/__init__.py +0 -0
  15. provekit/routers/auth.py +150 -0
  16. provekit/routers/connections.py +270 -0
  17. provekit/routers/deployments.py +161 -0
  18. provekit/routers/flows.py +188 -0
  19. provekit/routers/library.py +265 -0
  20. provekit/routers/prompts.py +72 -0
  21. provekit/routers/run.py +203 -0
  22. provekit/routers/runtime.py +103 -0
  23. provekit/routers/traces.py +65 -0
  24. provekit/routers/usage.py +42 -0
  25. provekit/services/__init__.py +0 -0
  26. provekit/services/assertions.py +135 -0
  27. provekit/services/auth.py +131 -0
  28. provekit/services/deploy.py +63 -0
  29. provekit/services/dispatch.py +313 -0
  30. provekit/services/email.py +38 -0
  31. provekit/services/flow.py +299 -0
  32. provekit/services/limits.py +109 -0
  33. provekit/services/masking.py +45 -0
  34. provekit/services/netguard.py +69 -0
  35. provekit/services/otel.py +146 -0
  36. provekit/services/promptfoo.py +92 -0
  37. provekit/services/providers/__init__.py +0 -0
  38. provekit/services/providers/a2a_client.py +110 -0
  39. provekit/services/providers/agent_http.py +55 -0
  40. provekit/services/providers/llm.py +364 -0
  41. provekit/services/providers/mcp_client.py +289 -0
  42. provekit/services/runstore.py +93 -0
  43. provekit/services/sealing.py +106 -0
  44. provekit/services/templates.py +69 -0
  45. provekit/services/testfile.py +122 -0
  46. provekit/services/tooling.py +194 -0
  47. provekit/services/workspace.py +91 -0
  48. provekit/templates/flows/_manifest.json +2594 -0
  49. provekit/templates/flows/banking-action-items.yaml +43 -0
  50. provekit/templates/flows/banking-answer.yaml +43 -0
  51. provekit/templates/flows/banking-classify-intent.yaml +43 -0
  52. provekit/templates/flows/banking-draft-critique-revise.yaml +75 -0
  53. provekit/templates/flows/banking-extract-to-json.yaml +43 -0
  54. provekit/templates/flows/banking-faq-match-answer.yaml +60 -0
  55. provekit/templates/flows/banking-keyword-extract.yaml +43 -0
  56. provekit/templates/flows/banking-language-route.yaml +103 -0
  57. provekit/templates/flows/banking-moderation-gate.yaml +73 -0
  58. provekit/templates/flows/banking-redact-summarize.yaml +59 -0
  59. provekit/templates/flows/banking-score-gate.yaml +73 -0
  60. provekit/templates/flows/banking-sentiment-route.yaml +73 -0
  61. provekit/templates/flows/banking-severity-tiers.yaml +103 -0
  62. provekit/templates/flows/banking-summarize-then-decide.yaml +88 -0
  63. provekit/templates/flows/banking-tag-enrich.yaml +43 -0
  64. provekit/templates/flows/banking-tone-rewrite.yaml +43 -0
  65. provekit/templates/flows/banking-translate-reply.yaml +58 -0
  66. provekit/templates/flows/banking-triage-route.yaml +73 -0
  67. provekit/templates/flows/community-forum-action-items.yaml +43 -0
  68. provekit/templates/flows/community-forum-answer.yaml +43 -0
  69. provekit/templates/flows/community-forum-classify-intent.yaml +43 -0
  70. provekit/templates/flows/community-forum-draft-critique-revise.yaml +75 -0
  71. provekit/templates/flows/community-forum-extract-to-json.yaml +43 -0
  72. provekit/templates/flows/community-forum-faq-match-answer.yaml +60 -0
  73. provekit/templates/flows/community-forum-keyword-extract.yaml +43 -0
  74. provekit/templates/flows/community-forum-language-route.yaml +103 -0
  75. provekit/templates/flows/community-forum-moderation-gate.yaml +73 -0
  76. provekit/templates/flows/community-forum-redact-summarize.yaml +59 -0
  77. provekit/templates/flows/community-forum-score-gate.yaml +73 -0
  78. provekit/templates/flows/community-forum-sentiment-route.yaml +73 -0
  79. provekit/templates/flows/community-forum-severity-tiers.yaml +103 -0
  80. provekit/templates/flows/community-forum-summarize-then-decide.yaml +88 -0
  81. provekit/templates/flows/community-forum-tag-enrich.yaml +43 -0
  82. provekit/templates/flows/community-forum-tone-rewrite.yaml +43 -0
  83. provekit/templates/flows/community-forum-translate-reply.yaml +58 -0
  84. provekit/templates/flows/community-forum-triage-route.yaml +73 -0
  85. provekit/templates/flows/content-moderation-action-items.yaml +43 -0
  86. provekit/templates/flows/content-moderation-answer.yaml +43 -0
  87. provekit/templates/flows/content-moderation-classify-intent.yaml +43 -0
  88. provekit/templates/flows/content-moderation-draft-critique-revise.yaml +75 -0
  89. provekit/templates/flows/content-moderation-extract-to-json.yaml +43 -0
  90. provekit/templates/flows/content-moderation-faq-match-answer.yaml +60 -0
  91. provekit/templates/flows/content-moderation-keyword-extract.yaml +43 -0
  92. provekit/templates/flows/content-moderation-language-route.yaml +103 -0
  93. provekit/templates/flows/content-moderation-moderation-gate.yaml +73 -0
  94. provekit/templates/flows/content-moderation-redact-summarize.yaml +59 -0
  95. provekit/templates/flows/content-moderation-score-gate.yaml +73 -0
  96. provekit/templates/flows/content-moderation-sentiment-route.yaml +73 -0
  97. provekit/templates/flows/content-moderation-severity-tiers.yaml +103 -0
  98. provekit/templates/flows/content-moderation-summarize-then-decide.yaml +88 -0
  99. provekit/templates/flows/content-moderation-tag-enrich.yaml +43 -0
  100. provekit/templates/flows/content-moderation-tone-rewrite.yaml +43 -0
  101. provekit/templates/flows/content-moderation-translate-reply.yaml +58 -0
  102. provekit/templates/flows/content-moderation-triage-route.yaml +73 -0
  103. provekit/templates/flows/customer-support-action-items.yaml +43 -0
  104. provekit/templates/flows/customer-support-answer.yaml +43 -0
  105. provekit/templates/flows/customer-support-classify-intent.yaml +43 -0
  106. provekit/templates/flows/customer-support-draft-critique-revise.yaml +75 -0
  107. provekit/templates/flows/customer-support-extract-to-json.yaml +43 -0
  108. provekit/templates/flows/customer-support-faq-match-answer.yaml +60 -0
  109. provekit/templates/flows/customer-support-keyword-extract.yaml +43 -0
  110. provekit/templates/flows/customer-support-language-route.yaml +103 -0
  111. provekit/templates/flows/customer-support-moderation-gate.yaml +73 -0
  112. provekit/templates/flows/customer-support-redact-summarize.yaml +59 -0
  113. provekit/templates/flows/customer-support-score-gate.yaml +73 -0
  114. provekit/templates/flows/customer-support-sentiment-route.yaml +73 -0
  115. provekit/templates/flows/customer-support-severity-tiers.yaml +103 -0
  116. provekit/templates/flows/customer-support-summarize-then-decide.yaml +88 -0
  117. provekit/templates/flows/customer-support-tag-enrich.yaml +43 -0
  118. provekit/templates/flows/customer-support-tone-rewrite.yaml +43 -0
  119. provekit/templates/flows/customer-support-translate-reply.yaml +58 -0
  120. provekit/templates/flows/customer-support-triage-route.yaml +73 -0
  121. provekit/templates/flows/developer-tooling-action-items.yaml +43 -0
  122. provekit/templates/flows/developer-tooling-answer.yaml +43 -0
  123. provekit/templates/flows/developer-tooling-classify-intent.yaml +43 -0
  124. provekit/templates/flows/developer-tooling-draft-critique-revise.yaml +75 -0
  125. provekit/templates/flows/developer-tooling-extract-to-json.yaml +43 -0
  126. provekit/templates/flows/developer-tooling-faq-match-answer.yaml +60 -0
  127. provekit/templates/flows/developer-tooling-keyword-extract.yaml +43 -0
  128. provekit/templates/flows/developer-tooling-language-route.yaml +103 -0
  129. provekit/templates/flows/developer-tooling-moderation-gate.yaml +73 -0
  130. provekit/templates/flows/developer-tooling-redact-summarize.yaml +59 -0
  131. provekit/templates/flows/developer-tooling-score-gate.yaml +73 -0
  132. provekit/templates/flows/developer-tooling-sentiment-route.yaml +73 -0
  133. provekit/templates/flows/developer-tooling-severity-tiers.yaml +103 -0
  134. provekit/templates/flows/developer-tooling-summarize-then-decide.yaml +88 -0
  135. provekit/templates/flows/developer-tooling-tag-enrich.yaml +43 -0
  136. provekit/templates/flows/developer-tooling-tone-rewrite.yaml +43 -0
  137. provekit/templates/flows/developer-tooling-translate-reply.yaml +58 -0
  138. provekit/templates/flows/developer-tooling-triage-route.yaml +73 -0
  139. provekit/templates/flows/devrel-action-items.yaml +43 -0
  140. provekit/templates/flows/devrel-answer.yaml +43 -0
  141. provekit/templates/flows/devrel-classify-intent.yaml +43 -0
  142. provekit/templates/flows/devrel-draft-critique-revise.yaml +75 -0
  143. provekit/templates/flows/devrel-extract-to-json.yaml +43 -0
  144. provekit/templates/flows/devrel-faq-match-answer.yaml +60 -0
  145. provekit/templates/flows/devrel-keyword-extract.yaml +43 -0
  146. provekit/templates/flows/devrel-language-route.yaml +103 -0
  147. provekit/templates/flows/devrel-moderation-gate.yaml +73 -0
  148. provekit/templates/flows/devrel-redact-summarize.yaml +59 -0
  149. provekit/templates/flows/devrel-score-gate.yaml +73 -0
  150. provekit/templates/flows/devrel-sentiment-route.yaml +73 -0
  151. provekit/templates/flows/devrel-severity-tiers.yaml +103 -0
  152. provekit/templates/flows/devrel-summarize-then-decide.yaml +88 -0
  153. provekit/templates/flows/devrel-tag-enrich.yaml +43 -0
  154. provekit/templates/flows/devrel-tone-rewrite.yaml +43 -0
  155. provekit/templates/flows/devrel-translate-reply.yaml +58 -0
  156. provekit/templates/flows/devrel-triage-route.yaml +73 -0
  157. provekit/templates/flows/e-commerce-action-items.yaml +43 -0
  158. provekit/templates/flows/e-commerce-answer.yaml +43 -0
  159. provekit/templates/flows/e-commerce-classify-intent.yaml +43 -0
  160. provekit/templates/flows/e-commerce-draft-critique-revise.yaml +75 -0
  161. provekit/templates/flows/e-commerce-extract-to-json.yaml +43 -0
  162. provekit/templates/flows/e-commerce-faq-match-answer.yaml +60 -0
  163. provekit/templates/flows/e-commerce-keyword-extract.yaml +43 -0
  164. provekit/templates/flows/e-commerce-language-route.yaml +103 -0
  165. provekit/templates/flows/e-commerce-moderation-gate.yaml +73 -0
  166. provekit/templates/flows/e-commerce-redact-summarize.yaml +59 -0
  167. provekit/templates/flows/e-commerce-score-gate.yaml +73 -0
  168. provekit/templates/flows/e-commerce-sentiment-route.yaml +73 -0
  169. provekit/templates/flows/e-commerce-severity-tiers.yaml +103 -0
  170. provekit/templates/flows/e-commerce-summarize-then-decide.yaml +88 -0
  171. provekit/templates/flows/e-commerce-tag-enrich.yaml +43 -0
  172. provekit/templates/flows/e-commerce-tone-rewrite.yaml +43 -0
  173. provekit/templates/flows/e-commerce-translate-reply.yaml +58 -0
  174. provekit/templates/flows/e-commerce-triage-route.yaml +73 -0
  175. provekit/templates/flows/education-action-items.yaml +43 -0
  176. provekit/templates/flows/education-answer.yaml +43 -0
  177. provekit/templates/flows/education-classify-intent.yaml +43 -0
  178. provekit/templates/flows/education-draft-critique-revise.yaml +75 -0
  179. provekit/templates/flows/education-extract-to-json.yaml +43 -0
  180. provekit/templates/flows/education-faq-match-answer.yaml +60 -0
  181. provekit/templates/flows/education-keyword-extract.yaml +43 -0
  182. provekit/templates/flows/education-language-route.yaml +103 -0
  183. provekit/templates/flows/education-moderation-gate.yaml +73 -0
  184. provekit/templates/flows/education-redact-summarize.yaml +59 -0
  185. provekit/templates/flows/education-score-gate.yaml +73 -0
  186. provekit/templates/flows/education-sentiment-route.yaml +73 -0
  187. provekit/templates/flows/education-severity-tiers.yaml +103 -0
  188. provekit/templates/flows/education-summarize-then-decide.yaml +88 -0
  189. provekit/templates/flows/education-tag-enrich.yaml +43 -0
  190. provekit/templates/flows/education-tone-rewrite.yaml +43 -0
  191. provekit/templates/flows/education-translate-reply.yaml +58 -0
  192. provekit/templates/flows/education-triage-route.yaml +73 -0
  193. provekit/templates/flows/finance-action-items.yaml +43 -0
  194. provekit/templates/flows/finance-answer.yaml +43 -0
  195. provekit/templates/flows/finance-classify-intent.yaml +43 -0
  196. provekit/templates/flows/finance-draft-critique-revise.yaml +75 -0
  197. provekit/templates/flows/finance-extract-to-json.yaml +43 -0
  198. provekit/templates/flows/finance-faq-match-answer.yaml +60 -0
  199. provekit/templates/flows/finance-keyword-extract.yaml +43 -0
  200. provekit/templates/flows/finance-language-route.yaml +103 -0
  201. provekit/templates/flows/finance-moderation-gate.yaml +73 -0
  202. provekit/templates/flows/finance-redact-summarize.yaml +59 -0
  203. provekit/templates/flows/finance-score-gate.yaml +73 -0
  204. provekit/templates/flows/finance-sentiment-route.yaml +73 -0
  205. provekit/templates/flows/finance-severity-tiers.yaml +103 -0
  206. provekit/templates/flows/finance-summarize-then-decide.yaml +88 -0
  207. provekit/templates/flows/finance-tag-enrich.yaml +43 -0
  208. provekit/templates/flows/finance-tone-rewrite.yaml +43 -0
  209. provekit/templates/flows/finance-translate-reply.yaml +58 -0
  210. provekit/templates/flows/finance-triage-route.yaml +73 -0
  211. provekit/templates/flows/food-delivery-action-items.yaml +43 -0
  212. provekit/templates/flows/food-delivery-answer.yaml +43 -0
  213. provekit/templates/flows/food-delivery-classify-intent.yaml +43 -0
  214. provekit/templates/flows/food-delivery-draft-critique-revise.yaml +75 -0
  215. provekit/templates/flows/food-delivery-extract-to-json.yaml +43 -0
  216. provekit/templates/flows/food-delivery-faq-match-answer.yaml +60 -0
  217. provekit/templates/flows/food-delivery-keyword-extract.yaml +43 -0
  218. provekit/templates/flows/food-delivery-language-route.yaml +103 -0
  219. provekit/templates/flows/food-delivery-moderation-gate.yaml +73 -0
  220. provekit/templates/flows/food-delivery-redact-summarize.yaml +59 -0
  221. provekit/templates/flows/food-delivery-score-gate.yaml +73 -0
  222. provekit/templates/flows/food-delivery-sentiment-route.yaml +73 -0
  223. provekit/templates/flows/food-delivery-severity-tiers.yaml +103 -0
  224. provekit/templates/flows/food-delivery-summarize-then-decide.yaml +88 -0
  225. provekit/templates/flows/food-delivery-tag-enrich.yaml +43 -0
  226. provekit/templates/flows/food-delivery-tone-rewrite.yaml +43 -0
  227. provekit/templates/flows/food-delivery-translate-reply.yaml +58 -0
  228. provekit/templates/flows/food-delivery-triage-route.yaml +73 -0
  229. provekit/templates/flows/gaming-support-action-items.yaml +43 -0
  230. provekit/templates/flows/gaming-support-answer.yaml +43 -0
  231. provekit/templates/flows/gaming-support-classify-intent.yaml +43 -0
  232. provekit/templates/flows/gaming-support-draft-critique-revise.yaml +75 -0
  233. provekit/templates/flows/gaming-support-extract-to-json.yaml +43 -0
  234. provekit/templates/flows/gaming-support-faq-match-answer.yaml +60 -0
  235. provekit/templates/flows/gaming-support-keyword-extract.yaml +43 -0
  236. provekit/templates/flows/gaming-support-language-route.yaml +103 -0
  237. provekit/templates/flows/gaming-support-moderation-gate.yaml +73 -0
  238. provekit/templates/flows/gaming-support-redact-summarize.yaml +59 -0
  239. provekit/templates/flows/gaming-support-score-gate.yaml +73 -0
  240. provekit/templates/flows/gaming-support-sentiment-route.yaml +73 -0
  241. provekit/templates/flows/gaming-support-severity-tiers.yaml +103 -0
  242. provekit/templates/flows/gaming-support-summarize-then-decide.yaml +88 -0
  243. provekit/templates/flows/gaming-support-tag-enrich.yaml +43 -0
  244. provekit/templates/flows/gaming-support-tone-rewrite.yaml +43 -0
  245. provekit/templates/flows/gaming-support-translate-reply.yaml +58 -0
  246. provekit/templates/flows/gaming-support-triage-route.yaml +73 -0
  247. provekit/templates/flows/healthcare-intake-action-items.yaml +43 -0
  248. provekit/templates/flows/healthcare-intake-answer.yaml +43 -0
  249. provekit/templates/flows/healthcare-intake-classify-intent.yaml +43 -0
  250. provekit/templates/flows/healthcare-intake-draft-critique-revise.yaml +75 -0
  251. provekit/templates/flows/healthcare-intake-extract-to-json.yaml +43 -0
  252. provekit/templates/flows/healthcare-intake-faq-match-answer.yaml +60 -0
  253. provekit/templates/flows/healthcare-intake-keyword-extract.yaml +43 -0
  254. provekit/templates/flows/healthcare-intake-language-route.yaml +103 -0
  255. provekit/templates/flows/healthcare-intake-moderation-gate.yaml +73 -0
  256. provekit/templates/flows/healthcare-intake-redact-summarize.yaml +59 -0
  257. provekit/templates/flows/healthcare-intake-score-gate.yaml +73 -0
  258. provekit/templates/flows/healthcare-intake-sentiment-route.yaml +73 -0
  259. provekit/templates/flows/healthcare-intake-severity-tiers.yaml +103 -0
  260. provekit/templates/flows/healthcare-intake-summarize-then-decide.yaml +88 -0
  261. provekit/templates/flows/healthcare-intake-tag-enrich.yaml +43 -0
  262. provekit/templates/flows/healthcare-intake-tone-rewrite.yaml +43 -0
  263. provekit/templates/flows/healthcare-intake-translate-reply.yaml +58 -0
  264. provekit/templates/flows/healthcare-intake-triage-route.yaml +73 -0
  265. provekit/templates/flows/hr-action-items.yaml +43 -0
  266. provekit/templates/flows/hr-answer.yaml +43 -0
  267. provekit/templates/flows/hr-classify-intent.yaml +43 -0
  268. provekit/templates/flows/hr-draft-critique-revise.yaml +75 -0
  269. provekit/templates/flows/hr-extract-to-json.yaml +43 -0
  270. provekit/templates/flows/hr-faq-match-answer.yaml +60 -0
  271. provekit/templates/flows/hr-keyword-extract.yaml +43 -0
  272. provekit/templates/flows/hr-language-route.yaml +103 -0
  273. provekit/templates/flows/hr-moderation-gate.yaml +73 -0
  274. provekit/templates/flows/hr-redact-summarize.yaml +59 -0
  275. provekit/templates/flows/hr-score-gate.yaml +73 -0
  276. provekit/templates/flows/hr-sentiment-route.yaml +73 -0
  277. provekit/templates/flows/hr-severity-tiers.yaml +103 -0
  278. provekit/templates/flows/hr-summarize-then-decide.yaml +88 -0
  279. provekit/templates/flows/hr-tag-enrich.yaml +43 -0
  280. provekit/templates/flows/hr-tone-rewrite.yaml +43 -0
  281. provekit/templates/flows/hr-translate-reply.yaml +58 -0
  282. provekit/templates/flows/hr-triage-route.yaml +73 -0
  283. provekit/templates/flows/insurance-action-items.yaml +43 -0
  284. provekit/templates/flows/insurance-answer.yaml +43 -0
  285. provekit/templates/flows/insurance-classify-intent.yaml +43 -0
  286. provekit/templates/flows/insurance-draft-critique-revise.yaml +75 -0
  287. provekit/templates/flows/insurance-extract-to-json.yaml +43 -0
  288. provekit/templates/flows/insurance-faq-match-answer.yaml +60 -0
  289. provekit/templates/flows/insurance-keyword-extract.yaml +43 -0
  290. provekit/templates/flows/insurance-language-route.yaml +103 -0
  291. provekit/templates/flows/insurance-moderation-gate.yaml +73 -0
  292. provekit/templates/flows/insurance-redact-summarize.yaml +59 -0
  293. provekit/templates/flows/insurance-score-gate.yaml +73 -0
  294. provekit/templates/flows/insurance-sentiment-route.yaml +73 -0
  295. provekit/templates/flows/insurance-severity-tiers.yaml +103 -0
  296. provekit/templates/flows/insurance-summarize-then-decide.yaml +88 -0
  297. provekit/templates/flows/insurance-tag-enrich.yaml +43 -0
  298. provekit/templates/flows/insurance-tone-rewrite.yaml +43 -0
  299. provekit/templates/flows/insurance-translate-reply.yaml +58 -0
  300. provekit/templates/flows/insurance-triage-route.yaml +73 -0
  301. provekit/templates/flows/it-helpdesk-action-items.yaml +43 -0
  302. provekit/templates/flows/it-helpdesk-answer.yaml +43 -0
  303. provekit/templates/flows/it-helpdesk-classify-intent.yaml +43 -0
  304. provekit/templates/flows/it-helpdesk-draft-critique-revise.yaml +75 -0
  305. provekit/templates/flows/it-helpdesk-extract-to-json.yaml +43 -0
  306. provekit/templates/flows/it-helpdesk-faq-match-answer.yaml +60 -0
  307. provekit/templates/flows/it-helpdesk-keyword-extract.yaml +43 -0
  308. provekit/templates/flows/it-helpdesk-language-route.yaml +103 -0
  309. provekit/templates/flows/it-helpdesk-moderation-gate.yaml +73 -0
  310. provekit/templates/flows/it-helpdesk-redact-summarize.yaml +59 -0
  311. provekit/templates/flows/it-helpdesk-score-gate.yaml +73 -0
  312. provekit/templates/flows/it-helpdesk-sentiment-route.yaml +73 -0
  313. provekit/templates/flows/it-helpdesk-severity-tiers.yaml +103 -0
  314. provekit/templates/flows/it-helpdesk-summarize-then-decide.yaml +88 -0
  315. provekit/templates/flows/it-helpdesk-tag-enrich.yaml +43 -0
  316. provekit/templates/flows/it-helpdesk-tone-rewrite.yaml +43 -0
  317. provekit/templates/flows/it-helpdesk-translate-reply.yaml +58 -0
  318. provekit/templates/flows/it-helpdesk-triage-route.yaml +73 -0
  319. provekit/templates/flows/legal-intake-action-items.yaml +43 -0
  320. provekit/templates/flows/legal-intake-answer.yaml +43 -0
  321. provekit/templates/flows/legal-intake-classify-intent.yaml +43 -0
  322. provekit/templates/flows/legal-intake-draft-critique-revise.yaml +75 -0
  323. provekit/templates/flows/legal-intake-extract-to-json.yaml +43 -0
  324. provekit/templates/flows/legal-intake-faq-match-answer.yaml +60 -0
  325. provekit/templates/flows/legal-intake-keyword-extract.yaml +43 -0
  326. provekit/templates/flows/legal-intake-language-route.yaml +103 -0
  327. provekit/templates/flows/legal-intake-moderation-gate.yaml +73 -0
  328. provekit/templates/flows/legal-intake-redact-summarize.yaml +59 -0
  329. provekit/templates/flows/legal-intake-score-gate.yaml +73 -0
  330. provekit/templates/flows/legal-intake-sentiment-route.yaml +73 -0
  331. provekit/templates/flows/legal-intake-severity-tiers.yaml +103 -0
  332. provekit/templates/flows/legal-intake-summarize-then-decide.yaml +88 -0
  333. provekit/templates/flows/legal-intake-tag-enrich.yaml +43 -0
  334. provekit/templates/flows/legal-intake-tone-rewrite.yaml +43 -0
  335. provekit/templates/flows/legal-intake-translate-reply.yaml +58 -0
  336. provekit/templates/flows/legal-intake-triage-route.yaml +73 -0
  337. provekit/templates/flows/logistics-action-items.yaml +43 -0
  338. provekit/templates/flows/logistics-answer.yaml +43 -0
  339. provekit/templates/flows/logistics-classify-intent.yaml +43 -0
  340. provekit/templates/flows/logistics-draft-critique-revise.yaml +75 -0
  341. provekit/templates/flows/logistics-extract-to-json.yaml +43 -0
  342. provekit/templates/flows/logistics-faq-match-answer.yaml +60 -0
  343. provekit/templates/flows/logistics-keyword-extract.yaml +43 -0
  344. provekit/templates/flows/logistics-language-route.yaml +103 -0
  345. provekit/templates/flows/logistics-moderation-gate.yaml +73 -0
  346. provekit/templates/flows/logistics-redact-summarize.yaml +59 -0
  347. provekit/templates/flows/logistics-score-gate.yaml +73 -0
  348. provekit/templates/flows/logistics-sentiment-route.yaml +73 -0
  349. provekit/templates/flows/logistics-severity-tiers.yaml +103 -0
  350. provekit/templates/flows/logistics-summarize-then-decide.yaml +88 -0
  351. provekit/templates/flows/logistics-tag-enrich.yaml +43 -0
  352. provekit/templates/flows/logistics-tone-rewrite.yaml +43 -0
  353. provekit/templates/flows/logistics-translate-reply.yaml +58 -0
  354. provekit/templates/flows/logistics-triage-route.yaml +73 -0
  355. provekit/templates/flows/marketing-action-items.yaml +43 -0
  356. provekit/templates/flows/marketing-answer.yaml +43 -0
  357. provekit/templates/flows/marketing-classify-intent.yaml +43 -0
  358. provekit/templates/flows/marketing-draft-critique-revise.yaml +75 -0
  359. provekit/templates/flows/marketing-extract-to-json.yaml +43 -0
  360. provekit/templates/flows/marketing-faq-match-answer.yaml +60 -0
  361. provekit/templates/flows/marketing-keyword-extract.yaml +43 -0
  362. provekit/templates/flows/marketing-language-route.yaml +103 -0
  363. provekit/templates/flows/marketing-moderation-gate.yaml +73 -0
  364. provekit/templates/flows/marketing-redact-summarize.yaml +59 -0
  365. provekit/templates/flows/marketing-score-gate.yaml +73 -0
  366. provekit/templates/flows/marketing-sentiment-route.yaml +73 -0
  367. provekit/templates/flows/marketing-severity-tiers.yaml +103 -0
  368. provekit/templates/flows/marketing-summarize-then-decide.yaml +88 -0
  369. provekit/templates/flows/marketing-tag-enrich.yaml +43 -0
  370. provekit/templates/flows/marketing-tone-rewrite.yaml +43 -0
  371. provekit/templates/flows/marketing-translate-reply.yaml +58 -0
  372. provekit/templates/flows/marketing-triage-route.yaml +73 -0
  373. provekit/templates/flows/nonprofit-action-items.yaml +43 -0
  374. provekit/templates/flows/nonprofit-answer.yaml +43 -0
  375. provekit/templates/flows/nonprofit-classify-intent.yaml +43 -0
  376. provekit/templates/flows/nonprofit-draft-critique-revise.yaml +75 -0
  377. provekit/templates/flows/nonprofit-extract-to-json.yaml +43 -0
  378. provekit/templates/flows/nonprofit-faq-match-answer.yaml +60 -0
  379. provekit/templates/flows/nonprofit-keyword-extract.yaml +43 -0
  380. provekit/templates/flows/nonprofit-language-route.yaml +103 -0
  381. provekit/templates/flows/nonprofit-moderation-gate.yaml +73 -0
  382. provekit/templates/flows/nonprofit-redact-summarize.yaml +59 -0
  383. provekit/templates/flows/nonprofit-score-gate.yaml +73 -0
  384. provekit/templates/flows/nonprofit-sentiment-route.yaml +73 -0
  385. provekit/templates/flows/nonprofit-severity-tiers.yaml +103 -0
  386. provekit/templates/flows/nonprofit-summarize-then-decide.yaml +88 -0
  387. provekit/templates/flows/nonprofit-tag-enrich.yaml +43 -0
  388. provekit/templates/flows/nonprofit-tone-rewrite.yaml +43 -0
  389. provekit/templates/flows/nonprofit-translate-reply.yaml +58 -0
  390. provekit/templates/flows/nonprofit-triage-route.yaml +73 -0
  391. provekit/templates/flows/real-estate-action-items.yaml +43 -0
  392. provekit/templates/flows/real-estate-answer.yaml +43 -0
  393. provekit/templates/flows/real-estate-classify-intent.yaml +43 -0
  394. provekit/templates/flows/real-estate-draft-critique-revise.yaml +75 -0
  395. provekit/templates/flows/real-estate-extract-to-json.yaml +43 -0
  396. provekit/templates/flows/real-estate-faq-match-answer.yaml +60 -0
  397. provekit/templates/flows/real-estate-keyword-extract.yaml +43 -0
  398. provekit/templates/flows/real-estate-language-route.yaml +103 -0
  399. provekit/templates/flows/real-estate-moderation-gate.yaml +73 -0
  400. provekit/templates/flows/real-estate-redact-summarize.yaml +59 -0
  401. provekit/templates/flows/real-estate-score-gate.yaml +73 -0
  402. provekit/templates/flows/real-estate-sentiment-route.yaml +73 -0
  403. provekit/templates/flows/real-estate-severity-tiers.yaml +103 -0
  404. provekit/templates/flows/real-estate-summarize-then-decide.yaml +88 -0
  405. provekit/templates/flows/real-estate-tag-enrich.yaml +43 -0
  406. provekit/templates/flows/real-estate-tone-rewrite.yaml +43 -0
  407. provekit/templates/flows/real-estate-translate-reply.yaml +58 -0
  408. provekit/templates/flows/real-estate-triage-route.yaml +73 -0
  409. provekit/templates/flows/recruiting-action-items.yaml +43 -0
  410. provekit/templates/flows/recruiting-answer.yaml +43 -0
  411. provekit/templates/flows/recruiting-classify-intent.yaml +43 -0
  412. provekit/templates/flows/recruiting-draft-critique-revise.yaml +75 -0
  413. provekit/templates/flows/recruiting-extract-to-json.yaml +43 -0
  414. provekit/templates/flows/recruiting-faq-match-answer.yaml +60 -0
  415. provekit/templates/flows/recruiting-keyword-extract.yaml +43 -0
  416. provekit/templates/flows/recruiting-language-route.yaml +103 -0
  417. provekit/templates/flows/recruiting-moderation-gate.yaml +73 -0
  418. provekit/templates/flows/recruiting-redact-summarize.yaml +59 -0
  419. provekit/templates/flows/recruiting-score-gate.yaml +73 -0
  420. provekit/templates/flows/recruiting-sentiment-route.yaml +73 -0
  421. provekit/templates/flows/recruiting-severity-tiers.yaml +103 -0
  422. provekit/templates/flows/recruiting-summarize-then-decide.yaml +88 -0
  423. provekit/templates/flows/recruiting-tag-enrich.yaml +43 -0
  424. provekit/templates/flows/recruiting-tone-rewrite.yaml +43 -0
  425. provekit/templates/flows/recruiting-translate-reply.yaml +58 -0
  426. provekit/templates/flows/recruiting-triage-route.yaml +73 -0
  427. provekit/templates/flows/research-action-items.yaml +43 -0
  428. provekit/templates/flows/research-answer.yaml +43 -0
  429. provekit/templates/flows/research-classify-intent.yaml +43 -0
  430. provekit/templates/flows/research-draft-critique-revise.yaml +75 -0
  431. provekit/templates/flows/research-extract-to-json.yaml +43 -0
  432. provekit/templates/flows/research-faq-match-answer.yaml +60 -0
  433. provekit/templates/flows/research-keyword-extract.yaml +43 -0
  434. provekit/templates/flows/research-language-route.yaml +103 -0
  435. provekit/templates/flows/research-moderation-gate.yaml +73 -0
  436. provekit/templates/flows/research-redact-summarize.yaml +59 -0
  437. provekit/templates/flows/research-score-gate.yaml +73 -0
  438. provekit/templates/flows/research-sentiment-route.yaml +73 -0
  439. provekit/templates/flows/research-severity-tiers.yaml +103 -0
  440. provekit/templates/flows/research-summarize-then-decide.yaml +88 -0
  441. provekit/templates/flows/research-tag-enrich.yaml +43 -0
  442. provekit/templates/flows/research-tone-rewrite.yaml +43 -0
  443. provekit/templates/flows/research-translate-reply.yaml +58 -0
  444. provekit/templates/flows/research-triage-route.yaml +73 -0
  445. provekit/templates/flows/sales-lead-action-items.yaml +43 -0
  446. provekit/templates/flows/sales-lead-answer.yaml +43 -0
  447. provekit/templates/flows/sales-lead-classify-intent.yaml +43 -0
  448. provekit/templates/flows/sales-lead-draft-critique-revise.yaml +75 -0
  449. provekit/templates/flows/sales-lead-extract-to-json.yaml +43 -0
  450. provekit/templates/flows/sales-lead-faq-match-answer.yaml +60 -0
  451. provekit/templates/flows/sales-lead-keyword-extract.yaml +43 -0
  452. provekit/templates/flows/sales-lead-language-route.yaml +103 -0
  453. provekit/templates/flows/sales-lead-moderation-gate.yaml +73 -0
  454. provekit/templates/flows/sales-lead-redact-summarize.yaml +59 -0
  455. provekit/templates/flows/sales-lead-score-gate.yaml +73 -0
  456. provekit/templates/flows/sales-lead-sentiment-route.yaml +73 -0
  457. provekit/templates/flows/sales-lead-severity-tiers.yaml +103 -0
  458. provekit/templates/flows/sales-lead-summarize-then-decide.yaml +88 -0
  459. provekit/templates/flows/sales-lead-tag-enrich.yaml +43 -0
  460. provekit/templates/flows/sales-lead-tone-rewrite.yaml +43 -0
  461. provekit/templates/flows/sales-lead-translate-reply.yaml +58 -0
  462. provekit/templates/flows/sales-lead-triage-route.yaml +73 -0
  463. provekit/templates/flows/travel-action-items.yaml +43 -0
  464. provekit/templates/flows/travel-answer.yaml +43 -0
  465. provekit/templates/flows/travel-classify-intent.yaml +43 -0
  466. provekit/templates/flows/travel-draft-critique-revise.yaml +75 -0
  467. provekit/templates/flows/travel-extract-to-json.yaml +43 -0
  468. provekit/templates/flows/travel-faq-match-answer.yaml +60 -0
  469. provekit/templates/flows/travel-keyword-extract.yaml +43 -0
  470. provekit/templates/flows/travel-language-route.yaml +103 -0
  471. provekit/templates/flows/travel-moderation-gate.yaml +73 -0
  472. provekit/templates/flows/travel-redact-summarize.yaml +59 -0
  473. provekit/templates/flows/travel-score-gate.yaml +73 -0
  474. provekit/templates/flows/travel-sentiment-route.yaml +73 -0
  475. provekit/templates/flows/travel-severity-tiers.yaml +103 -0
  476. provekit/templates/flows/travel-summarize-then-decide.yaml +88 -0
  477. provekit/templates/flows/travel-tag-enrich.yaml +43 -0
  478. provekit/templates/flows/travel-tone-rewrite.yaml +43 -0
  479. provekit/templates/flows/travel-translate-reply.yaml +58 -0
  480. provekit/templates/flows/travel-triage-route.yaml +73 -0
  481. provekit-0.1.0.dist-info/METADATA +86 -0
  482. provekit-0.1.0.dist-info/RECORD +485 -0
  483. provekit-0.1.0.dist-info/WHEEL +5 -0
  484. provekit-0.1.0.dist-info/entry_points.txt +2 -0
  485. provekit-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,131 @@
1
+ """Authentication: password hashing (PBKDF2), signed session tokens (HS256), and the
2
+ get_current_user dependency.
3
+
4
+ Local mode (HOSTED=false): auth is optional — an unauthenticated request is transparently
5
+ the built-in `local@provekit` user, so single-user local use needs no login.
6
+ Hosted mode (HOSTED=true): a valid session cookie is required; unauthenticated → 401.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import hashlib
12
+ import hmac
13
+ import json
14
+ import time
15
+
16
+ from fastapi import Depends, HTTPException, Request
17
+ from sqlalchemy.exc import IntegrityError
18
+ from sqlalchemy.orm import Session
19
+
20
+ from ..config import get_settings
21
+ from ..database import get_db
22
+ from ..models import User
23
+
24
+ COOKIE = "agm_session"
25
+ _TTL = 30 * 24 * 3600
26
+ LOCAL_EMAIL = "local@provekit"
27
+
28
+
29
+ def _b64(b: bytes) -> str:
30
+ return base64.urlsafe_b64encode(b).decode().rstrip("=")
31
+
32
+
33
+ def _unb64(s: str) -> bytes:
34
+ return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
35
+
36
+
37
+ def _secret() -> bytes:
38
+ s = get_settings().secret_key
39
+ if s:
40
+ return hashlib.sha256(("jwt:" + s).encode()).digest()
41
+ # Local: reuse the sealing key material so tokens are stable across restarts.
42
+ from .sealing import _fernet
43
+ return hashlib.sha256(b"jwt:" + _fernet()._signing_key).digest()
44
+
45
+
46
+ # ---- passwords (PBKDF2-HMAC-SHA256, stdlib) ----
47
+ def hash_password(pw: str, *, iterations: int = 200_000) -> str:
48
+ import os
49
+ salt = os.urandom(16)
50
+ dk = hashlib.pbkdf2_hmac("sha256", pw.encode(), salt, iterations)
51
+ return f"pbkdf2${iterations}${_b64(salt)}${_b64(dk)}"
52
+
53
+
54
+ def verify_password(pw: str, stored: str) -> bool:
55
+ try:
56
+ algo, iters, salt, dk = stored.split("$")
57
+ assert algo == "pbkdf2"
58
+ expected = hashlib.pbkdf2_hmac("sha256", pw.encode(), _unb64(salt), int(iters))
59
+ return hmac.compare_digest(expected, _unb64(dk))
60
+ except (ValueError, AssertionError):
61
+ return False
62
+
63
+
64
+ # Verify against this when the account is missing/OAuth-only, so the not-found login branch
65
+ # spends the same PBKDF2 time as a real check and doesn't leak account existence via timing.
66
+ DUMMY_HASH = hash_password("provekit-timing-equalizer")
67
+
68
+
69
+ # ---- signed tokens (compact HS256 JWT). purpose separates sessions from reset/verify;
70
+ # ver binds the token to the user's token_version so a password reset revokes old tokens. ----
71
+ def make_token(uid: int, ttl: int = _TTL, purpose: str = "session", ver: int = 0) -> str:
72
+ header = _b64(json.dumps({"alg": "HS256", "typ": "JWT"}, separators=(",", ":")).encode())
73
+ payload = _b64(json.dumps({"uid": uid, "exp": int(time.time()) + ttl, "p": purpose, "v": ver},
74
+ separators=(",", ":")).encode())
75
+ signing_input = f"{header}.{payload}".encode()
76
+ sig = _b64(hmac.new(_secret(), signing_input, hashlib.sha256).digest())
77
+ return f"{header}.{payload}.{sig}"
78
+
79
+
80
+ def read_token(token: str, purpose: str = "session") -> tuple[int, int] | None:
81
+ """Verify signature/expiry/purpose and return (user_id, token_version), else None. The
82
+ caller checks token_version against the user so revoked tokens are rejected."""
83
+ try:
84
+ header, payload, sig = token.split(".")
85
+ expected = _b64(hmac.new(_secret(), f"{header}.{payload}".encode(), hashlib.sha256).digest())
86
+ # compare_digest raises TypeError on a non-ASCII signature segment — treat as invalid.
87
+ if not hmac.compare_digest(expected, sig):
88
+ return None
89
+ data = json.loads(_unb64(payload))
90
+ if data.get("exp", 0) < time.time():
91
+ return None
92
+ if data.get("p", "session") != purpose: # a reset token can't be used as a session
93
+ return None
94
+ return int(data["uid"]), int(data.get("v", 0))
95
+ except (ValueError, KeyError, TypeError, json.JSONDecodeError):
96
+ return None
97
+
98
+
99
+ def _local_user(db: Session) -> User:
100
+ u = db.query(User).filter(User.email == LOCAL_EMAIL).first()
101
+ if u:
102
+ return u
103
+ u = User(email=LOCAL_EMAIL, name="Local", auth_provider="local")
104
+ db.add(u)
105
+ try:
106
+ db.commit()
107
+ except IntegrityError:
108
+ # A concurrent first request created it (the mount fires several requests at once);
109
+ # the unique-email constraint rejects the loser — fall back to the row that won.
110
+ db.rollback()
111
+ winner = db.query(User).filter(User.email == LOCAL_EMAIL).first()
112
+ if winner is None:
113
+ # Not the race we assumed: some other integrity failure. Returning None here
114
+ # would surface as an opaque AttributeError deep in workspace resolution.
115
+ raise
116
+ return winner
117
+ db.refresh(u)
118
+ return u
119
+
120
+
121
+ def get_current_user(request: Request, db: Session = Depends(get_db)) -> User:
122
+ token = request.cookies.get(COOKIE)
123
+ claims = read_token(token) if token else None
124
+ if claims is not None:
125
+ uid, ver = claims
126
+ u = db.get(User, uid)
127
+ if u and u.token_version == ver: # reject sessions revoked by a password reset
128
+ return u
129
+ if not get_settings().hosted:
130
+ return _local_user(db) # local mode: no login required
131
+ raise HTTPException(401, "Authentication required")
@@ -0,0 +1,63 @@
1
+ """Deployment helpers: API keys, slugs, and executing a frozen flow snapshot.
2
+
3
+ Deploying a flow snapshots its graph and mints an API key (shown once). Invoking a
4
+ deployment runs that snapshot through the same flow engine the canvas uses, so what you
5
+ tested is exactly what runs in production.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import hmac
11
+ import re
12
+ import secrets
13
+
14
+ from . import flow as engine
15
+
16
+
17
+ def new_api_key() -> tuple[str, str]:
18
+ """Return (plaintext, hash). Plaintext is shown to the user exactly once."""
19
+ key = "agm_" + secrets.token_urlsafe(32)
20
+ return key, hash_key(key)
21
+
22
+
23
+ def hash_key(key: str) -> str:
24
+ return hashlib.sha256(key.encode()).hexdigest()
25
+
26
+
27
+ def verify_key(key: str, key_hash: str) -> bool:
28
+ return hmac.compare_digest(hash_key(key), key_hash)
29
+
30
+
31
+ def slugify(name: str) -> str:
32
+ s = re.sub(r"[^a-z0-9]+", "-", (name or "").lower()).strip("-")
33
+ return s or "flow"
34
+
35
+
36
+ async def run_snapshot(session, snapshot: dict, flow_input: dict, workspace_id: int, stream=False):
37
+ """Execute a deployment's frozen flow. Yields the engine's events; the caller collects
38
+ the terminal output (output-node values) or passes events through for streaming."""
39
+ async for ev in engine.run_stream(session, {"nodes": snapshot.get("nodes", []),
40
+ "edges": snapshot.get("edges", [])},
41
+ flow_input or {}, workspace_id=workspace_id):
42
+ yield ev
43
+
44
+
45
+ def collect_output(events: list[dict]) -> dict:
46
+ """Reduce engine events to a deployment response: the output node values + status."""
47
+ outputs, status, error, run_id = {}, "completed", "", None
48
+ for ev in events:
49
+ if ev.get("type") == "start":
50
+ run_id = ev.get("run_id")
51
+ elif ev.get("type") == "node" and ev.get("status") == "ok" and ev.get("node_type") == "output":
52
+ val = (ev.get("output") or {}).get("value") if isinstance(ev.get("output"), dict) else ev.get("output")
53
+ outputs[ev.get("title") or ev.get("node_id")] = val
54
+ elif ev.get("type") == "node" and ev.get("status") == "error":
55
+ status, error = "failed", ev.get("error", "")
56
+ elif ev.get("type") in ("done", "error"):
57
+ if ev.get("status"):
58
+ status = ev["status"]
59
+ if ev.get("type") == "error":
60
+ status, error = "failed", ev.get("error", "")
61
+ # A single output node returns its value directly; multiple return a name→value map.
62
+ output = next(iter(outputs.values())) if len(outputs) == 1 else (outputs or None)
63
+ return {"output": output, "status": status, "error": error, "run_id": run_id}
@@ -0,0 +1,313 @@
1
+ """The unified run dispatcher. Routes a request (prompt | tool | agent) to the right
2
+ provider and normalizes everything to one event schema:
3
+
4
+ {"type":"start", "run_id", "request_type"}
5
+ {"type":"delta", "text"} # streamed tokens
6
+ {"type":"node", "data"} # passthrough agent SSE event
7
+ {"type":"result", "data", "meta"} # final structured output
8
+ {"type":"done", "status", "duration_ms"}
9
+ {"type":"error", "error"}
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ import time
15
+ import uuid
16
+
17
+ import anyio
18
+
19
+ from ..models import Connection
20
+ from . import tooling
21
+ from .masking import is_masked
22
+ from .providers import a2a_client, agent_http, llm, mcp_client
23
+
24
+
25
+ def _add_usage(total: dict, add: dict) -> dict:
26
+ """Sum token counts across the turns of a tool loop — a run reports one usage total,
27
+ but a tool-calling run makes N model calls.
28
+
29
+ Recurses into the nested breakdowns every provider ships (`*_tokens_details`,
30
+ Anthropic's cache counters). Summing only the top level would leave the totals
31
+ internally inconsistent — parents summed over N turns, children from the last turn
32
+ alone — which reads as real data in a product people use to measure cost.
33
+ """
34
+ if not total:
35
+ return dict(add or {})
36
+ out = dict(total)
37
+ for k, v in (add or {}).items():
38
+ prev = out.get(k)
39
+ if isinstance(v, dict) and isinstance(prev, dict):
40
+ out[k] = _add_usage(prev, v)
41
+ elif isinstance(v, (int, float)) and isinstance(prev, (int, float)) and not isinstance(v, bool):
42
+ out[k] = prev + v
43
+ else:
44
+ out[k] = v
45
+ return out
46
+
47
+
48
+ def _rounds(raw) -> int:
49
+ """max_tool_rounds, clamped. Absent → 5; explicit 0 means "never execute", so it must
50
+ survive (a plain `or 5` would turn it into 5). Null/garbage is a caller error worth
51
+ saying out loud rather than silently defaulting."""
52
+ if raw is None:
53
+ return 5
54
+ try:
55
+ n = int(raw)
56
+ except (TypeError, ValueError):
57
+ raise ValueError(f"max_tool_rounds must be a number, got {raw!r}")
58
+ return max(0, min(n, tooling.MAX_TOOL_ROUNDS))
59
+
60
+ _VAR = re.compile(r"\{\{\s*([\w.\-]+)\s*\}\}")
61
+
62
+
63
+ def interpolate(text, variables: dict):
64
+ if not isinstance(text, str):
65
+ return text
66
+ return _VAR.sub(lambda m: str(variables.get(m.group(1), m.group(0))), text)
67
+
68
+
69
+ def _interp_obj(obj, variables: dict):
70
+ if isinstance(obj, str):
71
+ return interpolate(obj, variables)
72
+ if isinstance(obj, list):
73
+ return [_interp_obj(x, variables) for x in obj]
74
+ if isinstance(obj, dict):
75
+ return {k: _interp_obj(v, variables) for k, v in obj.items()}
76
+ return obj
77
+
78
+
79
+ def _conn(db, cid, workspace_id=None) -> Connection | None:
80
+ if not cid:
81
+ return None
82
+ c = db.get(Connection, cid)
83
+ # Tenancy: never resolve a connection outside the caller's workspace (prevents using
84
+ # another tenant's stored credentials by passing its connection_id). workspace_id=None
85
+ # (CLI / single-tenant) skips the check.
86
+ if c and workspace_id is not None and c.workspace_id != workspace_id:
87
+ return None
88
+ return c
89
+
90
+
91
+ async def run_collect(db, req: dict, variables: dict | None = None, workspace_id=None) -> dict:
92
+ """Run a request to completion (non-streaming) and return the collected result —
93
+ used by the flow engine to execute prompt/tool/agent nodes."""
94
+ text, output, meta, status, err, dur = [], None, {}, "completed", "", 0
95
+ events = []
96
+ async for ev in run(db, req, variables, workspace_id):
97
+ t = ev["type"]
98
+ if t == "delta":
99
+ text.append(ev.get("text", ""))
100
+ elif t == "node":
101
+ # Keep node events: they carry the tool calls a `tool_called` assertion reads.
102
+ # Dropping them made that assertion inert for anything run through a flow.
103
+ events.append(ev.get("data"))
104
+ elif t == "result":
105
+ output, meta = ev.get("data"), ev.get("meta", {})
106
+ elif t == "error":
107
+ err = ev.get("error", "")
108
+ elif t == "done":
109
+ status, dur = ev.get("status", "completed"), ev.get("duration_ms", 0)
110
+ return {"text": "".join(text) or None, "output": output, "meta": meta, "status": status,
111
+ "error": err, "duration_ms": dur, "events": events}
112
+
113
+
114
+ def run_collect_sync(db, req: dict, variables: dict | None = None, workspace_id=None) -> dict:
115
+ """Sync bridge for the CLI: drive the async run to completion. Never call from a loop."""
116
+ return anyio.run(run_collect, db, req, variables, workspace_id)
117
+
118
+
119
+ async def run(db, req: dict, variables: dict | None = None, workspace_id=None):
120
+ variables = variables or {}
121
+ rtype = req.get("type")
122
+ run_id = uuid.uuid4().hex[:12]
123
+ yield {"type": "start", "run_id": run_id, "request_type": rtype}
124
+ t0 = time.monotonic()
125
+ try:
126
+ if rtype == "prompt":
127
+ gen = _run_prompt(db, req, variables, workspace_id)
128
+ elif rtype == "tool":
129
+ gen = _run_tool(db, req, variables, workspace_id)
130
+ elif rtype == "agent":
131
+ gen = _run_agent(db, req, variables, workspace_id)
132
+ elif rtype == "a2a":
133
+ gen = _run_a2a(db, req, variables, workspace_id)
134
+ else:
135
+ raise ValueError(f"unknown request type: {rtype!r}")
136
+ async for ev in gen:
137
+ yield ev
138
+ except Exception as exc:
139
+ yield {"type": "error", "error": str(exc)[:600]}
140
+ yield {"type": "done", "status": "failed", "duration_ms": round((time.monotonic() - t0) * 1000)}
141
+ return
142
+ yield {"type": "done", "status": "completed", "duration_ms": round((time.monotonic() - t0) * 1000)}
143
+
144
+
145
+ async def _run_prompt(db, req, variables, workspace_id=None):
146
+ conn = _conn(db, req.get("connection_id"), workspace_id)
147
+ cfg = (conn.config or {}) if conn else {}
148
+ if conn:
149
+ # A stored connection is authoritative for destination AND credentials.
150
+ # Honoring per-request base_url/api_key here would let any caller point the
151
+ # stored key at their own host (credential exfiltration).
152
+ provider = cfg.get("provider") or "openai"
153
+ base_url = cfg.get("base_url") or ""
154
+ api_key = cfg.get("api_key") or ""
155
+ else:
156
+ provider = req.get("provider") or "openai"
157
+ base_url = req.get("base_url") or ""
158
+ api_key = req.get("api_key") or ""
159
+ model = req.get("model") or (cfg.get("models") or ["gpt-4o-mini"])[0]
160
+ system = interpolate(req.get("system"), variables)
161
+
162
+ messages = []
163
+ for turn in req.get("messages") or []:
164
+ role = "assistant" if turn.get("role") == "assistant" else "user"
165
+ messages.append({"role": role, "content": interpolate(turn.get("content", ""), variables)})
166
+ user = interpolate(req.get("user", ""), variables)
167
+ # Anthropic rejects empty content blocks — when a messages[] history is supplied,
168
+ # only append the final user turn if it actually says something.
169
+ if user or not messages:
170
+ messages.append({"role": "user", "content": user})
171
+
172
+ # MCP tools the model may call, discovered up front so an unreachable server fails the
173
+ # run before any tokens are spent. The DB lookup is cheap and stays here; discovery is
174
+ # blocking (HTTP, or spawning a stdio server) and must not sit on the event loop.
175
+ plans = tooling.plan(db, req.get("tools"), workspace_id)
176
+ tools = await anyio.to_thread.run_sync(tooling.discover, plans) if plans else []
177
+ by_api = {t.api_name: t for t in tools}
178
+ defs = tooling.for_provider(tools, provider)
179
+ rounds_left = _rounds(req.get("max_tool_rounds"))
180
+ runner = tooling.Runner()
181
+
182
+ parts, usage = [], {}
183
+ # Sessions the loop opens must be torn down even if the model stream fails,
184
+ # or a stdio MCP subprocess would outlive the run.
185
+ try:
186
+ while True:
187
+ text, calls = [], []
188
+ async for ev in llm.astream(provider=provider, base_url=base_url, api_key=api_key, model=model,
189
+ system=system, messages=messages,
190
+ temperature=float(req.get("temperature", 0.7)),
191
+ max_tokens=int(req.get("max_tokens", 1024)), tools=defs):
192
+ if ev["type"] == "delta":
193
+ text.append(ev["text"]); parts.append(ev["text"])
194
+ yield {"type": "delta", "text": ev["text"]}
195
+ elif ev["type"] == "tool_call":
196
+ calls.append(ev["call"])
197
+ elif ev["type"] == "node":
198
+ yield ev # stop_reason and other provider events
199
+ elif ev["type"] == "usage":
200
+ usage = _add_usage(usage, ev["usage"]) # one result per run, N model turns
201
+ if not calls:
202
+ break
203
+ # Report the server's real tool name, not the sanitized one the model was given, so
204
+ # a `tool_called` assertion matches what the user sees in the MCP server.
205
+ yield {"type": "node", "data": {"tool_calls": [
206
+ {"id": c["id"], "name": (by_api[c["name"]].name if c["name"] in by_api else c["name"]),
207
+ "args": c["args"]} for c in calls]}}
208
+ # Only an explicit `execute: false` (or the cap) stops the run. A name we don't know
209
+ # is the model's mistake, not the user's intent, and is handled below as a failed
210
+ # tool turn so it can correct itself.
211
+ dry = [c for c in calls if c["name"] in by_api and not by_api[c["name"]].execute]
212
+ if dry or rounds_left <= 0:
213
+ reason = ("tool execution is off for this request" if dry else "hit max_tool_rounds")
214
+ yield {"type": "node", "data": {"tools_stopped": reason}}
215
+ break
216
+ rounds_left -= 1
217
+ messages = messages + [{"role": "assistant", "content": "".join(text),
218
+ "tool_calls": [{"id": c["id"], "name": c["name"], "args": c["args"]}
219
+ for c in calls]}]
220
+ for c in calls:
221
+ tool = by_api.get(c["name"])
222
+ if tool is None:
223
+ # A hallucinated tool. Feed the error back rather than ending the run: the
224
+ # model can pick a real tool on the next turn.
225
+ known = ", ".join(sorted(by_api)) or "none"
226
+ out, ok, err = (f"Tool error: no tool named '{c['name']}'. Available: {known}",
227
+ False, f"unknown tool '{c['name']}'")
228
+ name = c["name"]
229
+ else:
230
+ name = tool.name
231
+ try:
232
+ # MCP is sync and may spawn a process — never run it on the event loop.
233
+ out = await anyio.to_thread.run_sync(runner.call, tool, c["args"])
234
+ ok, err = True, ""
235
+ except Exception as exc: # a failed tool is a turn the model sees, not a dead run
236
+ out, ok, err = f"Tool error: {exc}", False, str(exc)
237
+ yield {"type": "node", "data": {"tool_result": {"name": name, "ok": ok,
238
+ "output": out, "error": err}}}
239
+ messages.append({"role": "tool", "tool_call_id": c["id"], "name": c["name"],
240
+ "content": out})
241
+ finally:
242
+ runner.close()
243
+ yield {"type": "result", "data": {"text": "".join(parts)},
244
+ "meta": {"provider": provider, "model": model, "usage": usage}}
245
+
246
+
247
+ async def _run_tool(db, req, variables, workspace_id=None):
248
+ conn = _conn(db, req.get("connection_id"), workspace_id)
249
+ cfg = (conn.config or {}) if conn else {}
250
+ name = req.get("tool")
251
+ if not name:
252
+ raise ValueError("tool run needs a tool name")
253
+ args = _interp_obj(req.get("args") or {}, variables)
254
+ sess = _mcp_session(cfg, req if not conn else {})
255
+ # MCP is a one-shot blocking call (incl. a possible stdio subprocess) — offload to a
256
+ # thread so the event loop isn't blocked for its duration.
257
+ result = await anyio.to_thread.run_sync(sess.call_tool, name, args)
258
+ yield {"type": "result", "data": result, "meta": {"tool": name, "url": cfg.get("url") or req.get("url")}}
259
+
260
+
261
+ def _mcp_session(cfg: dict, adhoc: dict):
262
+ """Build an MCP session from a connection config (stdio | http, spec, oauth). `adhoc`
263
+ supplies url only for connection-less ad-hoc runs (never carries stored secrets)."""
264
+ if cfg.get("command"): # stdio transport
265
+ return mcp_client.MCPSession(command=cfg["command"], args=cfg.get("args"), env=cfg.get("env"),
266
+ spec=cfg.get("spec", "auto"))
267
+ url = cfg.get("url") or adhoc.get("url")
268
+ if not url:
269
+ raise ValueError("tool run needs an MCP connection (or a url)")
270
+ return mcp_client.MCPSession(url, headers=cfg.get("headers"), spec=cfg.get("spec", "auto"),
271
+ oauth=cfg.get("oauth"))
272
+
273
+
274
+ async def _run_agent(db, req, variables, workspace_id=None):
275
+ conn = _conn(db, req.get("connection_id"), workspace_id)
276
+ # Same rule as prompts/tools: stored auth headers travel only to the stored base_url.
277
+ base = ((conn.config or {}).get("base_url") if conn else None) or (req.get("base_url") if not conn else None)
278
+ if not base:
279
+ raise ValueError("agent run needs an agent connection (or base_url)")
280
+ # Drop masked values (e.g. a run replayed from history) so the connection's real
281
+ # header wins instead of being overwritten by "••••1234".
282
+ req_headers = {k: v for k, v in (req.get("headers") or {}).items() if not is_masked(v)}
283
+ headers = {**((conn.config or {}).get("headers") or {} if conn else {}), **req_headers}
284
+ body = _interp_obj(req.get("body"), variables)
285
+ async for ev in agent_http.arun(base_url=base, method=req.get("method", "POST"),
286
+ path=interpolate(req.get("path", ""), variables),
287
+ headers=headers, body=body, stream=bool(req.get("stream"))):
288
+ if ev["type"] == "delta":
289
+ yield {"type": "delta", "text": ev["text"]}
290
+ elif ev["type"] == "event":
291
+ yield {"type": "node", "data": ev["data"]}
292
+ elif ev["type"] == "result":
293
+ yield {"type": "result", "data": ev.get("data"), "meta": ev.get("meta", {})}
294
+
295
+
296
+ async def _run_a2a(db, req, variables, workspace_id=None):
297
+ conn = _conn(db, req.get("connection_id"), workspace_id)
298
+ base = ((conn.config or {}).get("base_url") if conn else None) or (req.get("base_url") if not conn else None)
299
+ if not base:
300
+ raise ValueError("A2A run needs an A2A connection (or base_url)")
301
+ # Drop masked values on the ad-hoc path (a run replayed from history) so we don't send
302
+ # the literal "••••1234" placeholder as an auth header.
303
+ headers = ((conn.config or {}).get("headers") if conn
304
+ else {k: v for k, v in (req.get("headers") or {}).items() if not is_masked(v)} or None)
305
+ text = interpolate(req.get("message") or req.get("text") or "", variables)
306
+ card = None
307
+ try: # discover the endpoint from the agent card; fall back to base_url on failure
308
+ card = await a2a_client.fetch_card(base, headers=headers)
309
+ except Exception:
310
+ pass
311
+ async for ev in a2a_client.arun(base_url=base, text=text, headers=headers,
312
+ stream=bool(req.get("stream")), card=card):
313
+ yield ev
@@ -0,0 +1,38 @@
1
+ """Pluggable email sender. Sends via SMTP when configured, otherwise logs the message
2
+ (the zero-config default for local/self-host — the reset/verify link appears in the logs)."""
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import smtplib
7
+ from email.message import EmailMessage
8
+
9
+ from ..config import get_settings
10
+
11
+ log = logging.getLogger("provekit.email")
12
+
13
+
14
+ def send(to: str, subject: str, body: str) -> None:
15
+ s = get_settings()
16
+ if not s.smtp_host:
17
+ if s.hosted:
18
+ # Never write reset/verify links (the message body) to logs in a hosted
19
+ # deployment — anyone with log access could take over accounts. Fail loudly instead.
20
+ log.error("EMAIL NOT SENT: hosted mode has no SMTP configured (to=%s subject=%s)", to, subject)
21
+ else:
22
+ # Dev / self-host without SMTP: surface the message (incl. any link) in the logs.
23
+ log.info("EMAIL (no SMTP configured) to=%s subject=%s\n%s", to, subject, body)
24
+ return
25
+ msg = EmailMessage()
26
+ msg["From"] = s.smtp_from or s.smtp_user or "no-reply@provekit.local"
27
+ msg["To"] = to
28
+ msg["Subject"] = subject
29
+ msg.set_content(body)
30
+ try:
31
+ with smtplib.SMTP(s.smtp_host, s.smtp_port, timeout=15) as srv:
32
+ if s.smtp_starttls:
33
+ srv.starttls()
34
+ if s.smtp_user:
35
+ srv.login(s.smtp_user, s.smtp_password)
36
+ srv.send_message(msg)
37
+ except Exception as exc:
38
+ log.warning("email send failed to=%s: %s", to, exc)