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,299 @@
1
+ """Generic visual-flow engine. Walks a graph of nodes (input · prompt · tool · agent ·
2
+ condition · output), threading a shared context, and streams rich per-node events so the
3
+ canvas can animate + inspect execution. Nodes reuse the providers via dispatch.run_collect.
4
+ Supports breakpoints + single-step (like a debugger).
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import re
10
+ import time
11
+ import uuid
12
+
13
+ from . import dispatch
14
+ from .runstore import drop_ctx as _drop_run
15
+ from .runstore import pop_ctx
16
+ from .runstore import store_ctx as _store_run
17
+
18
+ MAX_STEPS = 100
19
+ _REF = re.compile(r"\{\{\s*([\w.\-]+)\s*\}\}")
20
+
21
+
22
+ def _skey(run_id: str, workspace_id) -> str:
23
+ """Namespace a stored run context by workspace so /continue can only resume runs from
24
+ the caller's own workspace — a foreign run_id misses the store instead of popping (and
25
+ deleting) another tenant's paused context."""
26
+ return f"{workspace_id if workspace_id is not None else 0}:{run_id}"
27
+
28
+
29
+ def pop_run(run_id: str, workspace_id=None) -> dict | None:
30
+ """Atomically remove and return a paused run context for a workspace (None if absent)."""
31
+ return pop_ctx(_skey(run_id, workspace_id))
32
+
33
+ # Catalog served to the frontend palette + inspector.
34
+ NODE_TYPES = {
35
+ "input": {"label": "Input", "category": "trigger", "color": "muted"},
36
+ "prompt": {"label": "Prompt", "category": "ai", "color": "prompt"},
37
+ "tool": {"label": "Tool", "category": "tool", "color": "tool"},
38
+ "agent": {"label": "Agent", "category": "agent", "color": "agent"},
39
+ "condition": {"label": "Condition", "category": "logic", "color": "purple", "branches": ["true", "false"]},
40
+ "output": {"label": "Output", "category": "output", "color": "ok"},
41
+ }
42
+
43
+
44
+ def _resolve(ref: str, ctx: dict):
45
+ parts = [p for p in ref.split(".") if p]
46
+ if not parts:
47
+ return None
48
+ root = parts[0]
49
+ cur = ctx.get("input") if root == "input" else (ctx.get("nodes") or {}).get(root)
50
+ for p in parts[1:]:
51
+ if isinstance(cur, dict):
52
+ cur = cur.get(p)
53
+ elif isinstance(cur, list):
54
+ try:
55
+ cur = cur[int(p)]
56
+ except (ValueError, IndexError):
57
+ return None
58
+ else:
59
+ return None
60
+ return cur
61
+
62
+
63
+ def _interp(text, ctx):
64
+ if not isinstance(text, str):
65
+ return text
66
+
67
+ def sub(m):
68
+ v = _resolve(m.group(1), ctx)
69
+ if v is None:
70
+ return m.group(0)
71
+ return v if isinstance(v, str) else json.dumps(v)
72
+ return _REF.sub(sub, text)
73
+
74
+
75
+ def _interp_obj(obj, ctx):
76
+ if isinstance(obj, str):
77
+ return _interp(obj, ctx)
78
+ if isinstance(obj, list):
79
+ return [_interp_obj(x, ctx) for x in obj]
80
+ if isinstance(obj, dict):
81
+ return {k: _interp_obj(v, ctx) for k, v in obj.items()}
82
+ return obj
83
+
84
+
85
+ def _mask_config(cfg):
86
+ """Mask secrets in a node's config before it's streamed to the canvas — an agent node's
87
+ inline `headers` or `body` can carry a credential that must not leak into run events."""
88
+ from .masking import mask_body, mask_headers
89
+ if not isinstance(cfg, dict):
90
+ return cfg
91
+ hdrs = cfg.get("headers")
92
+ out = mask_body({k: v for k, v in cfg.items() if k != "headers"})
93
+ if isinstance(hdrs, dict):
94
+ out["headers"] = mask_headers(hdrs)
95
+ return out
96
+
97
+
98
+ def _trim(v, _d=0):
99
+ if _d > 6:
100
+ return "…"
101
+ if isinstance(v, str):
102
+ return v if len(v) <= 2000 else v[:2000] + "…"
103
+ if isinstance(v, dict):
104
+ return {k: _trim(x, _d + 1) for k, x in list(v.items())[:50]}
105
+ if isinstance(v, list):
106
+ return [_trim(x, _d + 1) for x in v[:40]]
107
+ return v
108
+
109
+
110
+ def _adjacency(edges):
111
+ adj = {}
112
+ for e in edges:
113
+ adj.setdefault(e["source"], []).append(e)
114
+ return adj
115
+
116
+
117
+ def _next(adj, cur, branch):
118
+ outs = adj.get(cur, [])
119
+ if branch is not None:
120
+ for e in outs:
121
+ if (e.get("condition") or {}).get("branch") == branch:
122
+ return e["target"]
123
+ return None
124
+ uncond = [e for e in outs if not (e.get("condition") or {}).get("branch")]
125
+ return (uncond[0]["target"] if uncond else (outs[0]["target"] if outs else None))
126
+
127
+
128
+ def _find_trigger(graph):
129
+ indeg = {n["id"]: 0 for n in graph["nodes"]}
130
+ for e in graph["edges"]:
131
+ if e["target"] in indeg:
132
+ indeg[e["target"]] += 1
133
+ for n in graph["nodes"]:
134
+ if n["type"] == "input" and indeg.get(n["id"], 0) == 0:
135
+ return n["id"]
136
+ for n in graph["nodes"]:
137
+ if indeg.get(n["id"], 0) == 0:
138
+ return n["id"]
139
+ return graph["nodes"][0]["id"] if graph["nodes"] else None
140
+
141
+
142
+ async def _exec_node(db, node, ctx, variables=None, workspace_id=None):
143
+ """Execute one node; return (output, branch). `variables` (the active environment)
144
+ fill any {{name}} refs the flow context didn't resolve, matching console behavior."""
145
+ t = node["type"]
146
+ cfg = node.get("config") or {}
147
+ if t == "input":
148
+ return ctx.get("input") or {}, None
149
+ if t == "prompt":
150
+ system = cfg.get("system")
151
+ if cfg.get("prompt_key"): # pull the shared prompt from the registry (single source of truth)
152
+ reg = _registry_prompt(db, cfg["prompt_key"], workspace_id)
153
+ if reg is not None:
154
+ system = reg
155
+ req = {"type": "prompt", "connection_id": cfg.get("connection_id"), "model": cfg.get("model"),
156
+ "system": _interp(system, ctx), "user": _interp(cfg.get("user", ""), ctx),
157
+ "temperature": cfg.get("temperature", 0.7), "max_tokens": cfg.get("max_tokens", 1024)}
158
+ if cfg.get("tools"): # MCP servers this node's model may call
159
+ req["tools"] = cfg["tools"]
160
+ req["max_tool_rounds"] = cfg.get("max_tool_rounds", 5)
161
+ r = await dispatch.run_collect(db, req, variables, workspace_id)
162
+ if r["status"] == "failed":
163
+ raise RuntimeError(r["error"] or "prompt failed")
164
+ # Surface tool calls downstream only when there were some — a tool-free prompt node
165
+ # keeps its existing {"text": ...} output shape.
166
+ out = {"text": r["text"]}
167
+ if r.get("events"):
168
+ out["events"] = r["events"]
169
+ return out, None
170
+ if t == "tool":
171
+ req = {"type": "tool", "connection_id": cfg.get("connection_id"), "tool": cfg.get("tool"),
172
+ "args": _interp_obj(cfg.get("args") or {}, ctx)}
173
+ r = await dispatch.run_collect(db, req, variables, workspace_id)
174
+ if r["status"] == "failed":
175
+ raise RuntimeError(r["error"] or "tool failed")
176
+ return r["output"], None
177
+ if t == "agent":
178
+ req = {"type": "agent", "connection_id": cfg.get("connection_id"), "method": cfg.get("method", "POST"),
179
+ "path": _interp(cfg.get("path", ""), ctx), "headers": cfg.get("headers") or {},
180
+ "body": _interp_obj(cfg.get("body"), ctx)}
181
+ r = await dispatch.run_collect(db, req, variables, workspace_id)
182
+ if r["status"] == "failed":
183
+ raise RuntimeError(r["error"] or "agent failed")
184
+ return r["output"], None
185
+ if t == "condition":
186
+ # Unresolved refs become "" (not the literal "{{ref}}") so `exists`/comparisons are correct.
187
+ left = _interp_blank(cfg.get("left", ""), ctx)
188
+ right = _interp_blank(cfg.get("right", ""), ctx)
189
+ op = cfg.get("op", "==")
190
+ ok = _compare(left, right, op)
191
+ return {"result": ok, "left": left, "right": right, "op": op}, ("true" if ok else "false")
192
+ if t == "output":
193
+ val = _interp_obj(cfg.get("value", ""), ctx)
194
+ return {"value": val}, None
195
+ return {}, None
196
+
197
+
198
+ def _registry_prompt(db, key: str, workspace_id=None):
199
+ """Resolve a Prompt Registry entry's content by key within the workspace (None if missing)."""
200
+ from ..models import Prompt
201
+ q = db.query(Prompt).filter(Prompt.key == key)
202
+ if workspace_id is not None:
203
+ q = q.filter(Prompt.workspace_id == workspace_id)
204
+ row = q.first()
205
+ return row.content if row else None
206
+
207
+
208
+ def _interp_blank(text, ctx):
209
+ """Like _interp but resolves unknown refs to "" instead of echoing the literal template —
210
+ correct for condition comparisons / exists checks."""
211
+ if not isinstance(text, str):
212
+ return text
213
+
214
+ def sub(m):
215
+ v = _resolve(m.group(1), ctx)
216
+ if v is None:
217
+ return ""
218
+ return v if isinstance(v, str) else json.dumps(v)
219
+ return _REF.sub(sub, text)
220
+
221
+
222
+ def _compare(left, right, op):
223
+ if op == "exists":
224
+ return left not in (None, "", "null")
225
+ if op == "contains":
226
+ return str(right) in str(left)
227
+ if op in ("==", "equals"):
228
+ return str(left) == str(right)
229
+ if op == "!=":
230
+ return str(left) != str(right)
231
+ try:
232
+ lf, rf = float(left), float(right)
233
+ return lf > rf if op == ">" else lf < rf if op == "<" else False
234
+ except (ValueError, TypeError):
235
+ return False
236
+
237
+
238
+ async def run_stream(db, flow: dict, flow_input: dict, breakpoints=None, single_step=False,
239
+ start_at=None, ctx=None, run_id=None, variables=None, workspace_id=None):
240
+ breakpoints = set(breakpoints or [])
241
+ graph = {"nodes": flow["nodes"], "edges": flow["edges"]}
242
+ nodes = {n["id"]: n for n in graph["nodes"]}
243
+ adj = _adjacency(graph["edges"])
244
+ ctx = ctx or {"input": flow_input or {}, "nodes": {}}
245
+ rid = run_id or uuid.uuid4().hex[:12]
246
+ yield {"type": "start", "run_id": rid}
247
+
248
+ skey = _skey(rid, workspace_id)
249
+ node_id = start_at or _find_trigger(graph)
250
+ steps, first = ctx.get("_steps", 0), True # cumulative across pause/continue so cycles stay bounded
251
+ while node_id:
252
+ # Skip the breakpoint check only for the node we're resuming ONTO.
253
+ if node_id in breakpoints and not (first and start_at):
254
+ ctx["_steps"] = steps
255
+ _store_run(skey, ctx)
256
+ yield {"type": "pause", "node_id": node_id, "run_id": rid, "reason": "breakpoint"}
257
+ return
258
+ steps += 1
259
+ if steps > MAX_STEPS:
260
+ _drop_run(skey)
261
+ yield {"type": "error", "error": "step budget exceeded (cycle?)"}
262
+ yield {"type": "done", "status": "failed"}
263
+ return
264
+ node = nodes.get(node_id)
265
+ if not node:
266
+ break
267
+ ntype = node["type"]
268
+ title = (node.get("data") or {}).get("title") or NODE_TYPES.get(ntype, {}).get("label", ntype)
269
+ yield {"type": "node", "node_id": node_id, "node_type": ntype, "title": title, "status": "running"}
270
+ t0 = time.monotonic()
271
+ try:
272
+ output, branch = await _exec_node(db, node, ctx, variables, workspace_id)
273
+ except Exception as exc:
274
+ _drop_run(skey)
275
+ yield {"type": "node", "node_id": node_id, "node_type": ntype, "title": title,
276
+ "status": "error", "error": str(exc)[:400], "duration_ms": round((time.monotonic() - t0) * 1000)}
277
+ yield {"type": "done", "status": "failed"}
278
+ return
279
+ dur = round((time.monotonic() - t0) * 1000)
280
+ ctx["nodes"][node_id] = output
281
+ yield {"type": "node", "node_id": node_id, "node_type": ntype, "title": title, "status": "ok",
282
+ "branch": branch, "duration_ms": dur, "input": _trim(_mask_config(node.get("config") or {})),
283
+ "output": _trim(output)}
284
+ nxt = _next(adj, node_id, branch)
285
+ if single_step and nxt:
286
+ ctx["_steps"] = steps
287
+ _store_run(skey, ctx)
288
+ yield {"type": "pause", "node_id": nxt, "run_id": rid, "reason": "step"}
289
+ return
290
+ node_id = nxt
291
+ first = False
292
+
293
+ _drop_run(skey)
294
+ yield {"type": "done", "status": "completed", "output": _trim({k: v for k, v in (ctx.get("nodes") or {}).items()})}
295
+
296
+
297
+ # pop_run wraps the runstore's GETDEL-atomic pop with per-workspace key namespacing so a
298
+ # concurrent /continue can't resume twice and a foreign run_id can't touch another tenant.
299
+ __all__ = ["run_stream", "pop_run", "NODE_TYPES"]
@@ -0,0 +1,109 @@
1
+ """Rate limiting + quotas, so one workspace can't exhaust shared infrastructure.
2
+
3
+ Fixed-window per-workspace counter. Redis-backed when REDIS_URL is set (correct across
4
+ workers), else in-memory. A dependency raises 429 with Retry-After when the window is full.
5
+ Local mode (no hosting) leaves the limit generous; tune via RATE_LIMIT_PER_MIN.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import threading
10
+ import time
11
+ from collections import OrderedDict
12
+ from functools import lru_cache
13
+
14
+ from fastapi import Depends, HTTPException
15
+
16
+ from ..config import get_settings
17
+ from ..models import Workspace
18
+ from .workspace import current_workspace
19
+
20
+
21
+ class _MemoryWindow:
22
+ def __init__(self):
23
+ # LRU-ordered so eviction drops the oldest bucket, never the one being counted.
24
+ self._c: OrderedDict[str, int] = OrderedDict()
25
+ self._lock = threading.Lock()
26
+
27
+ def hit(self, key: str, ttl: int) -> int:
28
+ # Streams run concurrently in the threadpool, so the read-modify-write must be atomic
29
+ # or concurrent hits under-count and admit more than the limit.
30
+ with self._lock:
31
+ count = self._c.get(key, 0) + 1
32
+ self._c[key] = count
33
+ self._c.move_to_end(key) # mark as most-recently-used
34
+ while len(self._c) > 10_000:
35
+ self._c.popitem(last=False) # evict least-recently-used, keeping live buckets
36
+ return count
37
+
38
+
39
+ class _RedisWindow:
40
+ def __init__(self, url: str):
41
+ import redis
42
+ self._r = redis.Redis.from_url(url, decode_responses=True)
43
+
44
+ def hit(self, key: str, ttl: int) -> int:
45
+ pipe = self._r.pipeline()
46
+ pipe.incr(key)
47
+ pipe.expire(key, ttl)
48
+ return pipe.execute()[0]
49
+
50
+
51
+ @lru_cache
52
+ def _window():
53
+ url = get_settings().redis_url
54
+ return _RedisWindow(url) if url else _MemoryWindow()
55
+
56
+
57
+ def _now() -> int:
58
+ # time.time() is fine here (not a resumable workflow); windows are wall-clock minutes.
59
+ return int(time.time())
60
+
61
+
62
+ def check_rate(ws: Workspace = Depends(current_workspace)) -> Workspace:
63
+ """Dependency: enforce the per-workspace request rate on run endpoints."""
64
+ limit = get_settings().rate_limit_per_min
65
+ if limit <= 0:
66
+ return ws
67
+ window = _now() // 60
68
+ key = f"rl:{ws.id}:{window}"
69
+ count = _window().hit(key, 60)
70
+ if count > limit:
71
+ raise HTTPException(429, "Rate limit exceeded — slow down.",
72
+ headers={"Retry-After": str(60 - (_now() % 60))})
73
+ return ws
74
+
75
+
76
+ def check_login_rate(ident: str) -> None:
77
+ """Throttle login attempts per identifier (email+IP) to blunt brute force."""
78
+ limit = get_settings().login_attempts_per_min
79
+ if limit <= 0:
80
+ return
81
+ key = f"login:{ident}:{_now() // 60}"
82
+ if _window().hit(key, 60) > limit:
83
+ raise HTTPException(429, "Too many login attempts — wait a minute.",
84
+ headers={"Retry-After": str(60 - (_now() % 60))})
85
+
86
+
87
+ def enforce_dataset_size(n_rows: int) -> None:
88
+ cap = get_settings().dataset_max_rows
89
+ if cap and n_rows > cap:
90
+ raise HTTPException(400, f"Dataset too large: {n_rows} rows (max {cap}).")
91
+
92
+
93
+ def clamp_max_tokens(req: dict) -> None:
94
+ cap = get_settings().max_tokens_cap
95
+ if cap and isinstance(req.get("max_tokens"), int) and req["max_tokens"] > cap:
96
+ req["max_tokens"] = cap
97
+
98
+
99
+ def prune_runs(db, workspace_id: int) -> None:
100
+ """Keep only the most recent N runs per workspace (called after an insert)."""
101
+ keep = get_settings().runs_retention
102
+ if not keep:
103
+ return
104
+ from ..models import Run
105
+ ids = [r.id for r in db.query(Run.id).filter(Run.workspace_id == workspace_id)
106
+ .order_by(Run.id.desc()).offset(keep).all()]
107
+ if ids:
108
+ db.query(Run).filter(Run.id.in_(ids)).delete(synchronize_session=False)
109
+ db.commit()
@@ -0,0 +1,45 @@
1
+ """Masking helpers for secret values in API responses and persisted records."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+
6
+ MASK = "••••••"
7
+
8
+ # Header keys whose values are secrets — masked in API responses and run history.
9
+ SECRET_HEADERS = {"authorization", "x-api-key", "api-key", "cookie", "x-auth-token", "proxy-authorization"}
10
+ # Also treat any header whose name looks credential-bearing as secret (e.g. X-Custom-Token),
11
+ # so masking isn't limited to the fixed set above.
12
+ _SECRET_HEADER_RE = re.compile(r"(authorization|api[-_]?key|secret|token|password|passwd|cookie|credential|session)", re.I)
13
+
14
+
15
+ def is_secret_header(name: str) -> bool:
16
+ n = str(name).lower()
17
+ return n in SECRET_HEADERS or bool(_SECRET_HEADER_RE.search(n))
18
+
19
+
20
+ def mask_value(v) -> str:
21
+ s = str(v)
22
+ return MASK + s[-4:] if len(s) > 4 else MASK
23
+
24
+
25
+ def mask_headers(headers: dict) -> dict:
26
+ return {k: (mask_value(v) if is_secret_header(k) and v else v) for k, v in headers.items()}
27
+
28
+
29
+ def is_masked(v) -> bool:
30
+ return isinstance(v, str) and v.startswith(MASK)
31
+
32
+
33
+ # Body fields whose values look like secrets — masked before persisting to history.
34
+ _SECRET_FIELDS = {"api_key", "apikey", "token", "access_token", "refresh_token", "password",
35
+ "secret", "client_secret", "authorization"}
36
+
37
+
38
+ def mask_body(obj):
39
+ """Recursively mask secret-looking fields in a request/response body for storage."""
40
+ if isinstance(obj, dict):
41
+ return {k: (mask_value(v) if k.lower() in _SECRET_FIELDS and v and isinstance(v, str)
42
+ else mask_body(v)) for k, v in obj.items()}
43
+ if isinstance(obj, list):
44
+ return [mask_body(x) for x in obj]
45
+ return obj
@@ -0,0 +1,69 @@
1
+ """Outbound URL guard (SSRF protection), enforced at every provider entry point.
2
+
3
+ Local mode (default): this is a dev tool that legitimately targets localhost and
4
+ private IPs, so those stay allowed — only the link-local block (incl. the
5
+ 169.254.169.254 cloud-metadata endpoint) is refused.
6
+
7
+ Hosted mode (HOSTED=true): all private/reserved ranges are blocked, and hostnames
8
+ are DNS-resolved so a name pointing at an internal IP is caught too. A rebinding
9
+ server could still answer differently on the actual connect — pinning connections
10
+ through an egress proxy is the complete fix; accepted residual risk pre-GA.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import ipaddress
15
+ import socket
16
+ from urllib.parse import urlparse
17
+
18
+ from ..config import get_settings
19
+
20
+ _BLOCKED_HOSTS = {"metadata", "metadata.google.internal"}
21
+
22
+
23
+ class BlockedURL(ValueError):
24
+ """Raised when an outbound URL targets a forbidden address."""
25
+
26
+
27
+ def guard_stdio() -> None:
28
+ """Refuse to spawn a local MCP server process in hosted mode.
29
+
30
+ stdio MCP connections run an arbitrary `command` on the server (subprocess.Popen).
31
+ That is fine for a single-user local install, but on shared/hosted infrastructure it
32
+ is remote code execution: any authenticated tenant could run arbitrary programs. URLs
33
+ are policed by guard_url; this is the equivalent gate for the process transport.
34
+ """
35
+ if get_settings().hosted:
36
+ raise BlockedURL("Local process (stdio) MCP connections are disabled in hosted mode")
37
+
38
+
39
+ def _check_ip(ip, hosted: bool) -> None:
40
+ if ip.is_link_local:
41
+ raise BlockedURL("Link-local / metadata addresses are not allowed")
42
+ if hosted and not ip.is_global:
43
+ raise BlockedURL("Private / internal addresses are not allowed in hosted mode")
44
+
45
+
46
+ def guard_url(url: str) -> None:
47
+ hosted = get_settings().hosted
48
+ host = (urlparse(str(url)).hostname or "").lower()
49
+ if not host:
50
+ raise BlockedURL("URL has no host")
51
+ if host in _BLOCKED_HOSTS:
52
+ raise BlockedURL("Target host is not allowed")
53
+
54
+ ip = None
55
+ try:
56
+ ip = ipaddress.ip_address(host)
57
+ except ValueError:
58
+ pass # not a literal IP — a hostname
59
+ if ip is not None:
60
+ _check_ip(ip, hosted)
61
+ return
62
+
63
+ if hosted:
64
+ try:
65
+ infos = socket.getaddrinfo(host, None)
66
+ except OSError:
67
+ raise BlockedURL(f"Cannot resolve host '{host}'")
68
+ for info in infos:
69
+ _check_ip(ipaddress.ip_address(info[4][0]), hosted)
@@ -0,0 +1,146 @@
1
+ """OpenTelemetry GenAI ingest + emit.
2
+
3
+ Ingest: accept OTLP/HTTP-JSON trace exports and map gen_ai.* spans into ProveKit runs,
4
+ so a user SEES traces from an agent built in ANY framework (ADK, Strands, Pydantic AI,
5
+ CrewAI, Semantic Kernel, OpenAI Agents SDK…) without swapping in our SDK.
6
+
7
+ The gen_ai semantic conventions are still churning, so the mapping is deliberately thin
8
+ and accepts three dialects:
9
+ - current gen_ai.* v1.37+ (gen_ai.input.messages / gen_ai.output.messages,
10
+ gen_ai.provider.name, gen_ai.request.model, gen_ai.usage.*)
11
+ - legacy gen_ai.prompt / gen_ai.completion / gen_ai.system
12
+ - OpenInference llm.* / input.value / output.value
13
+
14
+ Emit: optionally export ProveKit's own runs as gen_ai spans to an OTLP collector, the
15
+ dual-export escape hatch buyers now expect.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import logging
21
+
22
+ log = logging.getLogger("provekit.otel")
23
+
24
+
25
+ def _attr_value(v: dict):
26
+ if "stringValue" in v:
27
+ return v["stringValue"]
28
+ if "intValue" in v:
29
+ return int(v["intValue"])
30
+ if "doubleValue" in v:
31
+ return v["doubleValue"]
32
+ if "boolValue" in v:
33
+ return v["boolValue"]
34
+ if "arrayValue" in v:
35
+ return [_attr_value(x) for x in v["arrayValue"].get("values", [])]
36
+ if "kvlistValue" in v:
37
+ return {kv["key"]: _attr_value(kv["value"]) for kv in v["kvlistValue"].get("values", [])}
38
+ return None
39
+
40
+
41
+ def flatten_attrs(attributes: list) -> dict:
42
+ return {a["key"]: _attr_value(a.get("value") or {}) for a in attributes or []}
43
+
44
+
45
+ def _first(attrs: dict, *keys):
46
+ for k in keys:
47
+ if k in attrs and attrs[k] not in (None, ""):
48
+ return attrs[k]
49
+ return None
50
+
51
+
52
+ def _as_text(v) -> str:
53
+ if v is None:
54
+ return ""
55
+ if isinstance(v, str):
56
+ return v
57
+ try:
58
+ return json.dumps(v)
59
+ except (TypeError, ValueError):
60
+ return str(v)
61
+
62
+
63
+ def map_span(span: dict) -> dict | None:
64
+ """Map one OTLP span to Run kwargs, or None if it isn't a gen_ai/LLM span."""
65
+ attrs = flatten_attrs(span.get("attributes"))
66
+ model = _first(attrs, "gen_ai.request.model", "gen_ai.response.model", "llm.model_name", "gen_ai.model")
67
+ provider = _first(attrs, "gen_ai.provider.name", "gen_ai.system", "llm.provider")
68
+ operation = _first(attrs, "gen_ai.operation.name")
69
+ tool = _first(attrs, "gen_ai.tool.name")
70
+ is_genai = bool(model or provider or operation or tool
71
+ or any(k.startswith(("gen_ai.", "llm.")) for k in attrs))
72
+ if not is_genai:
73
+ return None
74
+
75
+ prompt = _first(attrs, "gen_ai.input.messages", "gen_ai.prompt", "input.value", "llm.input_messages")
76
+ completion = _first(attrs, "gen_ai.output.messages", "gen_ai.completion", "output.value", "llm.output_messages")
77
+ usage = {}
78
+ it = _first(attrs, "gen_ai.usage.input_tokens", "gen_ai.usage.prompt_tokens", "llm.token_count.prompt")
79
+ ot = _first(attrs, "gen_ai.usage.output_tokens", "gen_ai.usage.completion_tokens", "llm.token_count.completion")
80
+ if it is not None:
81
+ usage["input_tokens"] = it
82
+ if ot is not None:
83
+ usage["output_tokens"] = ot
84
+
85
+ start = int(span.get("startTimeUnixNano") or 0)
86
+ end = int(span.get("endTimeUnixNano") or 0)
87
+ dur_ms = round((end - start) / 1e6) if end > start else 0
88
+ status = "failed" if (span.get("status") or {}).get("code") == 2 else "completed"
89
+
90
+ op = operation or ("execute_tool" if tool else "chat")
91
+ label = f"{op} {model}" if model else (span.get("name") or op)
92
+ meta = {"provider": provider, "model": model, "usage": usage, "source": "otel"}
93
+ if tool:
94
+ meta["tool"] = tool
95
+ return {
96
+ "type": "trace",
97
+ "label": label[:200],
98
+ "request": {"type": "trace", "provider": provider, "model": model,
99
+ "operation": op, "input": _as_text(prompt)[:8000]},
100
+ "result": {"text": _as_text(completion) or None, "output": None, "meta": meta},
101
+ "status": status,
102
+ "duration_ms": dur_ms,
103
+ "error": (span.get("status") or {}).get("message", "") if status == "failed" else "",
104
+ }
105
+
106
+
107
+ def ingest(payload: dict) -> list[dict]:
108
+ """Turn an OTLP ExportTraceServiceRequest into a list of Run kwargs (gen_ai spans only)."""
109
+ runs = []
110
+ for rs in payload.get("resourceSpans", []):
111
+ for ss in rs.get("scopeSpans", []) + rs.get("instrumentationLibrarySpans", []):
112
+ for span in ss.get("spans", []):
113
+ mapped = map_span(span)
114
+ if mapped:
115
+ runs.append(mapped)
116
+ return runs
117
+
118
+
119
+ # ---- emit (best-effort) ----
120
+ def emit_run(run_row) -> None:
121
+ """Export one ProveKit run as a gen_ai span to an OTLP collector, if configured."""
122
+ from ..config import get_settings
123
+ url = get_settings().otel_export_url
124
+ if not url:
125
+ return
126
+ import httpx
127
+
128
+ from .netguard import guard_url
129
+ meta = (run_row.result or {}).get("meta") or {}
130
+ attrs = [{"key": "gen_ai.operation.name", "value": {"stringValue": run_row.type}}]
131
+ if meta.get("provider"):
132
+ attrs.append({"key": "gen_ai.provider.name", "value": {"stringValue": str(meta["provider"])}})
133
+ if meta.get("model"):
134
+ attrs.append({"key": "gen_ai.request.model", "value": {"stringValue": str(meta["model"])}})
135
+ usage = meta.get("usage") or {}
136
+ for src, dst in (("input_tokens", "gen_ai.usage.input_tokens"), ("output_tokens", "gen_ai.usage.output_tokens")):
137
+ if usage.get(src) is not None:
138
+ attrs.append({"key": dst, "value": {"intValue": int(usage[src])}})
139
+ body = {"resourceSpans": [{"scopeSpans": [{"spans": [{
140
+ "name": run_row.label or run_row.type, "attributes": attrs,
141
+ "status": {"code": 2 if run_row.status == "failed" else 1}}]}]}]}
142
+ try:
143
+ guard_url(url)
144
+ httpx.post(url, json=body, timeout=5, follow_redirects=False)
145
+ except Exception as exc: # best-effort; never break a run on export failure
146
+ log.debug("otel emit failed: %s", exc)