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,203 @@
1
+ """Run a request (prompt | tool | agent), streaming unified events, evaluating any
2
+ assertions, and persisting to history. Also batch datasets (run over N input rows)."""
3
+ import json
4
+
5
+ import anyio
6
+ from fastapi import APIRouter, Depends, HTTPException
7
+ from fastapi.responses import StreamingResponse
8
+ from pydantic import BaseModel
9
+ from sqlalchemy.orm import Session
10
+
11
+ from ..database import SessionLocal, get_db
12
+ from ..models import Environment, Run, Workspace, iso_utc
13
+ from ..services import assertions as assertion_engine
14
+ from ..services import dispatch, otel
15
+ from ..services.limits import check_rate, clamp_max_tokens, enforce_dataset_size, prune_runs
16
+ from ..services.masking import mask_body, mask_headers
17
+ from ..services.workspace import current_workspace
18
+
19
+ router = APIRouter(prefix="/api", tags=["run"])
20
+
21
+
22
+ class RunPayload(BaseModel):
23
+ request: dict
24
+ variables: dict = {}
25
+ save: bool = True
26
+
27
+
28
+ class DatasetPayload(BaseModel):
29
+ request: dict
30
+ rows: list[dict] = []
31
+
32
+
33
+ def _active_vars(db: Session, ws_id: int) -> dict:
34
+ e = db.query(Environment).filter(Environment.workspace_id == ws_id, Environment.is_active.is_(True)).first()
35
+ return dict(e.variables or {}) if e else {}
36
+
37
+
38
+ def _label(req: dict) -> str:
39
+ t = req.get("type")
40
+ if t == "prompt":
41
+ return f"{req.get('model', 'prompt')}: {(req.get('user') or '')[:60]}"
42
+ if t == "tool":
43
+ return f"tool: {req.get('tool', '?')}"
44
+ if t == "agent":
45
+ return f"{req.get('method', 'POST')} {req.get('path', '')}"
46
+ return t or "run"
47
+
48
+
49
+ def _sanitize(req: dict) -> dict:
50
+ """Strip/mask secrets before persisting to run history (returned by GET /runs/{id})."""
51
+ out = {k: v for k, v in req.items() if k != "api_key"}
52
+ if isinstance(out.get("headers"), dict):
53
+ out["headers"] = mask_headers(out["headers"])
54
+ if out.get("body") is not None: # agent bodies can carry tokens/passwords
55
+ out["body"] = mask_body(out["body"])
56
+ return out
57
+
58
+
59
+ def _new_acc():
60
+ # status starts as "interrupted" — only a `done` event sets the real outcome, so a
61
+ # client disconnect mid-stream persists honestly instead of as "completed".
62
+ return {"parts": [], "output": None, "meta": {}, "events": [], "status": "interrupted", "dur": 0, "err": ""}
63
+
64
+
65
+ def _apply(acc, ev):
66
+ et = ev["type"]
67
+ if et == "delta":
68
+ acc["parts"].append(ev.get("text", ""))
69
+ elif et == "result":
70
+ acc["output"], acc["meta"] = ev.get("data"), ev.get("meta", {})
71
+ elif et == "node":
72
+ acc["events"].append(ev.get("data"))
73
+ elif et == "error":
74
+ acc["err"] = ev.get("error", "")
75
+ elif et == "done":
76
+ acc["status"], acc["dur"] = ev.get("status", "completed"), ev.get("duration_ms", 0)
77
+
78
+
79
+ def _run_dict(acc):
80
+ result = {"text": "".join(acc["parts"]) or None, "output": acc["output"], "meta": acc["meta"]}
81
+ if acc["events"]:
82
+ result["events"] = acc["events"]
83
+ return {"result": result, "status": acc["status"], "duration_ms": acc["dur"], "events": acc["events"], "error": acc["err"]}
84
+
85
+
86
+ async def _collect(db, req, variables, ws_id):
87
+ acc = _new_acc()
88
+ async for ev in dispatch.run(db, req, variables, ws_id):
89
+ _apply(acc, ev)
90
+ return _run_dict(acc)
91
+
92
+
93
+ async def _evaluate(db, assertions, rd, ws_id):
94
+ """Offload assertion evaluation to a thread — the llm_judge assertion runs its own
95
+ event loop (asyncio.run), which can't happen inside this request's loop."""
96
+ if not assertions:
97
+ return []
98
+ return await anyio.to_thread.run_sync(assertion_engine.evaluate, db, assertions, rd, ws_id)
99
+
100
+
101
+ async def _persist_async(db, req, rd, asserts, ws_id):
102
+ """Persist off the event loop — a blocking DB commit (esp. a contended SQLite write)
103
+ must not stall every other in-flight stream sharing this loop."""
104
+ await anyio.to_thread.run_sync(_persist, db, req, rd, asserts, ws_id)
105
+
106
+
107
+ def _persist(db, req, rd, asserts, ws_id):
108
+ result = dict(rd["result"])
109
+ if asserts:
110
+ result["assertions"] = asserts
111
+ try:
112
+ row = Run(workspace_id=ws_id, type=req.get("type", "?"), label=_label(req), request=_sanitize(req),
113
+ result=result, status=rd["status"], duration_ms=rd["duration_ms"], error=rd["error"])
114
+ db.add(row); db.commit()
115
+ otel.emit_run(row) # best-effort mirror to an OTLP collector if configured
116
+ prune_runs(db, ws_id)
117
+ except Exception:
118
+ db.rollback()
119
+
120
+
121
+ @router.post("/run/stream")
122
+ def run_stream(payload: RunPayload, db: Session = Depends(get_db), ws: Workspace = Depends(check_rate)):
123
+ variables = {**_active_vars(db, ws.id), **(payload.variables or {})}
124
+ req = payload.request
125
+ clamp_max_tokens(req)
126
+ assertions = req.get("assertions") or []
127
+ ws_id = ws.id
128
+
129
+ async def live():
130
+ # Fresh session: FastAPI tears down the request's Depends(get_db) before this
131
+ # generator runs (dependencies-with-yield exit when the endpoint returns).
132
+ session = SessionLocal()
133
+ acc = _new_acc()
134
+ asserts: list = []
135
+ try:
136
+ async for ev in dispatch.run(session, req, variables, ws_id):
137
+ _apply(acc, ev)
138
+ yield f"data: {json.dumps(ev)}\n\n"
139
+ rd = _run_dict(acc)
140
+ asserts = await _evaluate(session, assertions, rd, ws_id)
141
+ if asserts:
142
+ yield f"data: {json.dumps({'type': 'assert', 'results': asserts})}\n\n"
143
+ yield "data: [DONE]\n\n"
144
+ finally:
145
+ # Runs also on client disconnect (GeneratorExit) — history keeps the partial run.
146
+ try:
147
+ if payload.save:
148
+ await _persist_async(session, req, _run_dict(acc), asserts, ws_id)
149
+ finally:
150
+ session.close()
151
+
152
+ return StreamingResponse(live(), media_type="text/event-stream",
153
+ headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
154
+
155
+
156
+ @router.post("/run")
157
+ async def run_once(payload: RunPayload, db: Session = Depends(get_db), ws: Workspace = Depends(check_rate)):
158
+ clamp_max_tokens(payload.request)
159
+ variables = {**_active_vars(db, ws.id), **(payload.variables or {})}
160
+ rd = await _collect(db, payload.request, variables, ws.id)
161
+ asserts = await _evaluate(db, payload.request.get("assertions") or [], rd, ws.id)
162
+ if payload.save:
163
+ await _persist_async(db, payload.request, rd, asserts, ws.id)
164
+ return {"result": rd["result"], "status": rd["status"], "duration_ms": rd["duration_ms"], "assertions": asserts}
165
+
166
+
167
+ @router.post("/dataset/run")
168
+ async def dataset_run(payload: DatasetPayload, db: Session = Depends(get_db), ws: Workspace = Depends(check_rate)):
169
+ enforce_dataset_size(len(payload.rows))
170
+ clamp_max_tokens(payload.request)
171
+ assertions = payload.request.get("assertions") or []
172
+ base = _active_vars(db, ws.id)
173
+ rows = []
174
+ for i, row in enumerate(payload.rows):
175
+ variables = {**base, **(row.get("variables") or {})}
176
+ rd = await _collect(db, payload.request, variables, ws.id)
177
+ asserts = await _evaluate(db, assertions, rd, ws.id)
178
+ passed = all(a["ok"] for a in asserts) if asserts else rd["status"] == "completed"
179
+ rows.append({
180
+ "name": row.get("name") or f"row {i + 1}", "status": rd["status"],
181
+ "text": rd["result"].get("text"), "output": rd["result"].get("output"),
182
+ "assertions": asserts, "pass": passed, "duration_ms": rd["duration_ms"],
183
+ })
184
+ return {"rows": rows, "summary": {"passed": sum(1 for r in rows if r["pass"]), "total": len(rows)}}
185
+
186
+
187
+ @router.get("/runs")
188
+ def list_runs(limit: int = 30, db: Session = Depends(get_db), ws: Workspace = Depends(current_workspace)):
189
+ limit = max(1, min(limit, 200)) # bound the page (SQLite treats LIMIT -1 as unlimited)
190
+ rows = db.query(Run).filter(Run.workspace_id == ws.id).order_by(Run.id.desc()).limit(limit).all()
191
+ return [{"id": r.id, "type": r.type, "label": r.label, "status": r.status,
192
+ "duration_ms": r.duration_ms, "created_at": iso_utc(r.created_at)}
193
+ for r in rows]
194
+
195
+
196
+ @router.get("/runs/{rid}")
197
+ def get_run(rid: int, db: Session = Depends(get_db), ws: Workspace = Depends(current_workspace)):
198
+ r = db.get(Run, rid)
199
+ if not r or r.workspace_id != ws.id:
200
+ raise HTTPException(404, "Run not found")
201
+ return {"id": r.id, "type": r.type, "label": r.label, "request": r.request,
202
+ "result": r.result, "status": r.status, "duration_ms": r.duration_ms,
203
+ "error": r.error, "created_at": iso_utc(r.created_at)}
@@ -0,0 +1,103 @@
1
+ """Public runtime — invoke a deployed flow by slug with an API key. No session auth.
2
+
3
+ POST /v1/d/{slug} → JSON {output, status, run_id, duration_ms}
4
+ POST /v1/d/{slug}?stream=1 → SSE passthrough of the flow's node/delta events
5
+
6
+ This executes user-defined outbound calls, so it depends on the P0 safeguards
7
+ (credential-override block, SSRF guard, secret masking) already in place.
8
+ """
9
+ import json
10
+ import time
11
+
12
+ import anyio
13
+ from fastapi import APIRouter, Header, HTTPException, Request
14
+ from fastapi.responses import StreamingResponse
15
+
16
+ from ..database import SessionLocal
17
+ from ..models import Deployment, Run
18
+ from ..services import deploy
19
+ from ..services.limits import _window
20
+ from ..config import get_settings
21
+
22
+ router = APIRouter(prefix="/v1/d", tags=["runtime"])
23
+
24
+
25
+ def _resolve(session, slug: str, api_key: str | None) -> Deployment:
26
+ d = (session.query(Deployment)
27
+ .filter(Deployment.slug == slug, Deployment.active.is_(True))
28
+ .order_by(Deployment.version.desc()).first())
29
+ if not d:
30
+ # Distinguish "never existed / deactivated" without leaking which.
31
+ any_row = session.query(Deployment).filter(Deployment.slug == slug).first()
32
+ raise HTTPException(410 if any_row else 404, "Deployment not available")
33
+ if not api_key or not deploy.verify_key(api_key, d.api_key_hash):
34
+ raise HTTPException(403, "Invalid API key")
35
+ return d
36
+
37
+
38
+ def _rate_ok(deployment_id: int) -> bool:
39
+ limit = get_settings().rate_limit_per_min
40
+ if limit <= 0:
41
+ return True
42
+ key = f"rl:dep:{deployment_id}:{int(time.time()) // 60}"
43
+ return _window().hit(key, 60) <= limit
44
+
45
+
46
+ @router.post("/{slug}")
47
+ async def invoke(slug: str, request: Request, stream: bool = False,
48
+ x_api_key: str | None = Header(default=None)):
49
+ body = {}
50
+ try:
51
+ body = await request.json()
52
+ except Exception:
53
+ pass
54
+
55
+ session = SessionLocal()
56
+ try:
57
+ d = _resolve(session, slug, x_api_key)
58
+ if not _rate_ok(d.id):
59
+ raise HTTPException(429, "Rate limit exceeded")
60
+ snapshot, ws_id, dep_id = d.snapshot, d.workspace_id, d.id
61
+ except HTTPException:
62
+ session.close()
63
+ raise
64
+
65
+ if stream:
66
+ async def gen():
67
+ try:
68
+ async for ev in deploy.run_snapshot(session, snapshot, body, ws_id):
69
+ yield f"data: {json.dumps(ev)}\n\n"
70
+ yield "data: [DONE]\n\n"
71
+ finally:
72
+ session.close()
73
+ return StreamingResponse(gen(), media_type="text/event-stream",
74
+ headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
75
+
76
+ try:
77
+ t0 = time.monotonic()
78
+ timeout = get_settings().deployment_timeout_s
79
+ timed_out = False
80
+ events = []
81
+ with anyio.move_on_after(timeout if timeout > 0 else None) as scope:
82
+ async for ev in deploy.run_snapshot(session, snapshot, body, ws_id):
83
+ events.append(ev)
84
+ timed_out = scope.cancelled_caught
85
+ result = deploy.collect_output(events)
86
+ if timed_out:
87
+ result["status"] = "failed"
88
+ result["error"] = f"deployment invocation exceeded {timeout}s timeout"
89
+ dur = round((time.monotonic() - t0) * 1000)
90
+
91
+ def _save():
92
+ session.add(Run(workspace_id=ws_id, deployment_id=dep_id, type="deployment",
93
+ label=f"deploy {slug}", request={"input": body},
94
+ result={"output": result["output"]}, status=result["status"],
95
+ duration_ms=dur, error=result["error"]))
96
+ session.commit()
97
+ await anyio.to_thread.run_sync(_save) # keep the blocking write off the loop
98
+ if timed_out:
99
+ raise HTTPException(504, result["error"])
100
+ return {"output": result["output"], "status": result["status"],
101
+ "run_id": result["run_id"], "duration_ms": dur}
102
+ finally:
103
+ session.close()
@@ -0,0 +1,65 @@
1
+ """OTLP/HTTP trace ingest — point any OpenTelemetry GenAI exporter at /v1/traces and
2
+ its LLM/agent spans show up in ProveKit's history, no SDK swap required.
3
+
4
+ Auth: a workspace ingest key via `Authorization: Bearer <key>` (what real exporters can
5
+ send), or a session cookie for interactive/local use. Mint the key from
6
+ POST /api/workspace/ingest-key."""
7
+ from fastapi import APIRouter, Depends, Header, HTTPException, Request
8
+ from pydantic import BaseModel
9
+ from sqlalchemy.orm import Session
10
+
11
+ from ..config import get_settings
12
+ from ..database import get_db
13
+ from ..models import Run, Workspace
14
+ from ..services import deploy, otel
15
+ from ..services.workspace import current_workspace
16
+
17
+ router = APIRouter(prefix="/v1", tags=["traces"])
18
+ ws_router = APIRouter(prefix="/api/workspace", tags=["workspace"])
19
+
20
+
21
+ def _resolve_ingest_ws(db: Session, request: Request, authorization: str | None) -> Workspace:
22
+ """Bearer ingest key first (exporters), else fall back to the session cookie."""
23
+ if authorization and authorization.lower().startswith("bearer "):
24
+ key = authorization[7:].strip()
25
+ h = deploy.hash_key(key)
26
+ ws = db.query(Workspace).filter(Workspace.ingest_key_hash == h).first()
27
+ if ws:
28
+ return ws
29
+ raise HTTPException(403, "Invalid ingest key")
30
+ # No bearer key: only allowed via a logged-in session (or local mode's default user).
31
+ from ..services.auth import get_current_user
32
+ from ..services.workspace import get_or_create_default_workspace
33
+ user = get_current_user(request, db)
34
+ return get_or_create_default_workspace(db, user)
35
+
36
+
37
+ @router.post("/traces")
38
+ async def ingest_traces(request: Request, db: Session = Depends(get_db),
39
+ authorization: str | None = Header(default=None)):
40
+ """Accept an OTLP ExportTraceServiceRequest (JSON) and persist gen_ai spans as runs.
41
+ Returns the OTLP success shape so standard exporters are satisfied."""
42
+ ws = _resolve_ingest_ws(db, request, authorization)
43
+ try:
44
+ payload = await request.json()
45
+ except Exception:
46
+ return {"partialSuccess": {"rejectedSpans": 0, "errorMessage": "invalid JSON"}}
47
+ rows = otel.ingest(payload)
48
+ for kw in rows:
49
+ db.add(Run(workspace_id=ws.id, **kw))
50
+ if rows:
51
+ db.commit()
52
+ return {"partialSuccess": {}}
53
+
54
+
55
+ class _KeyOut(BaseModel):
56
+ ingest_key: str
57
+
58
+
59
+ @ws_router.post("/ingest-key", response_model=_KeyOut)
60
+ def rotate_ingest_key(db: Session = Depends(get_db), ws: Workspace = Depends(current_workspace)):
61
+ """Mint/rotate this workspace's OTLP ingest key (shown once; stored hashed)."""
62
+ plaintext, key_hash = deploy.new_api_key()
63
+ ws.ingest_key_hash = key_hash
64
+ db.commit()
65
+ return {"ingest_key": plaintext}
@@ -0,0 +1,42 @@
1
+ """Per-workspace token usage aggregation — the foundation for quotas/billing later.
2
+ Normalizes the different providers' usage shapes (OpenAI prompt/completion, Anthropic
3
+ and Responses input/output) into one in/out count."""
4
+ from datetime import datetime, timedelta, timezone
5
+
6
+ from fastapi import APIRouter, Depends
7
+ from sqlalchemy.orm import Session
8
+
9
+ from ..database import get_db
10
+ from ..models import Run, Workspace
11
+ from ..services.workspace import current_workspace
12
+
13
+ router = APIRouter(prefix="/api/usage", tags=["usage"])
14
+
15
+
16
+ def _tokens(usage: dict) -> tuple[int, int]:
17
+ if not isinstance(usage, dict):
18
+ return 0, 0
19
+ tin = usage.get("input_tokens") or usage.get("prompt_tokens") or 0
20
+ tout = usage.get("output_tokens") or usage.get("completion_tokens") or 0
21
+ return int(tin or 0), int(tout or 0)
22
+
23
+
24
+ @router.get("")
25
+ def usage(days: int = 30, db: Session = Depends(get_db), ws: Workspace = Depends(current_workspace)):
26
+ """Totals + per-model breakdown of runs and tokens over the last `days`."""
27
+ since = datetime.now(timezone.utc) - timedelta(days=max(1, days))
28
+ rows = (db.query(Run).filter(Run.workspace_id == ws.id, Run.created_at >= since.replace(tzinfo=None)).all())
29
+ total_in = total_out = 0
30
+ by_model: dict[str, dict] = {}
31
+ for r in rows:
32
+ meta = (r.result or {}).get("meta") or {}
33
+ model = meta.get("model") or "—"
34
+ tin, tout = _tokens(meta.get("usage"))
35
+ total_in += tin; total_out += tout
36
+ m = by_model.setdefault(model, {"runs": 0, "tokens_in": 0, "tokens_out": 0})
37
+ m["runs"] += 1; m["tokens_in"] += tin; m["tokens_out"] += tout
38
+ return {
39
+ "window_days": days, "runs": len(rows),
40
+ "tokens_in": total_in, "tokens_out": total_out, "tokens_total": total_in + total_out,
41
+ "by_model": [{"model": k, **v} for k, v in sorted(by_model.items(), key=lambda kv: -kv[1]["tokens_in"] - kv[1]["tokens_out"])],
42
+ }
File without changes
@@ -0,0 +1,135 @@
1
+ """Assertions / evals — check a run's result against a list of assertions. Types:
2
+ contains · equals · regex · json_path · json_schema · tool_called · latency_lt · llm_judge.
3
+ Mirrors the eval assertions in tools like Reticle/LangSmith."""
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import re
8
+
9
+ from jsonschema import ValidationError
10
+ from jsonschema import validate as js_validate
11
+
12
+ from ..models import Connection
13
+ from .providers import llm
14
+
15
+ # re.search holds the GIL, so a catastrophic-backtracking pattern (ReDoS) would freeze the
16
+ # whole process — and a worker thread can't preempt it. Since stdlib re has no timeout, we
17
+ # reject the common nested-quantifier signature and cap the searched text instead. This is a
18
+ # heuristic guard; an interruptible engine (the `regex` module's timeout, or RE2) is the
19
+ # complete fix if fully-untrusted patterns ever need to run.
20
+ _MAX_REGEX_INPUT = 100_000
21
+ _NESTED_QUANT = re.compile(r"\([^()]*[*+][^()]*\)\s*[*+{]")
22
+
23
+
24
+ def _regex_search(pattern: str, text: str):
25
+ if len(pattern) > 2000:
26
+ raise ValueError("regex pattern too long")
27
+ if _NESTED_QUANT.search(pattern):
28
+ raise ValueError("regex rejected: nested quantifier (possible ReDoS)")
29
+ return re.search(pattern, text[:_MAX_REGEX_INPUT])
30
+
31
+
32
+ def _text_of(result: dict) -> str:
33
+ if result.get("text"):
34
+ return result["text"]
35
+ out = result.get("output")
36
+ if isinstance(out, str):
37
+ return out
38
+ return json.dumps(out) if out is not None else ""
39
+
40
+
41
+ def get_path(obj, path: str):
42
+ cur = obj
43
+ for part in str(path).split("."):
44
+ if part == "":
45
+ continue
46
+ if isinstance(cur, list):
47
+ try:
48
+ cur = cur[int(part)]
49
+ except (ValueError, IndexError):
50
+ return None
51
+ elif isinstance(cur, dict):
52
+ cur = cur.get(part)
53
+ else:
54
+ return None
55
+ return cur
56
+
57
+
58
+ def _llm_judge(db, a: dict, text: str, workspace_id=None):
59
+ conn = db.get(Connection, a.get("connection_id")) if a.get("connection_id") else None
60
+ if conn and workspace_id is not None and conn.workspace_id != workspace_id:
61
+ conn = None # tenancy: don't judge with another workspace's connection
62
+ if not conn:
63
+ return False, "no judge connection configured"
64
+ cfg = conn.config or {}
65
+ model = a.get("model") or (cfg.get("models") or ["gpt-4o-mini"])[0]
66
+ system = ("You are a strict evaluator. Given CRITERIA and OUTPUT, decide if the output "
67
+ "satisfies the criteria. Reply with exactly PASS or FAIL on the first line, "
68
+ "then a one-sentence reason.")
69
+ user = f"CRITERIA:\n{a.get('criteria', '')}\n\nOUTPUT:\n{text[:4000]}"
70
+ # Sync bridge (asyncio.run): evaluate() runs in a worker thread from async endpoints
71
+ # and directly in the CLI — both have no running loop, so this is safe.
72
+ reply = llm.collect_text_sync(provider=cfg.get("provider", "openai"), base_url=cfg.get("base_url", ""),
73
+ api_key=cfg.get("api_key", ""), model=model, system=system,
74
+ messages=[{"role": "user", "content": user}], temperature=0, max_tokens=200).strip()
75
+ return reply.upper().startswith("PASS"), reply[:160]
76
+
77
+
78
+ def evaluate(db, assertions: list, run: dict, workspace_id=None) -> list[dict]:
79
+ """run = {result:{text,output,meta}, status, duration_ms, events}."""
80
+ result = run.get("result", {}) or {}
81
+ text = _text_of(result)
82
+ output = result.get("output")
83
+ meta = result.get("meta", {}) or {}
84
+ dur = run.get("duration_ms", 0)
85
+ events = run.get("events") or result.get("events") or []
86
+ out = []
87
+ for a in assertions or []:
88
+ t = a.get("type")
89
+ ok, detail = False, ""
90
+ try:
91
+ if t == "contains":
92
+ ok = str(a.get("value", "")) in text
93
+ detail = f"output {'contains' if ok else 'missing'} “{a.get('value')}”"
94
+ elif t == "equals":
95
+ target = get_path(output, a["path"]) if a.get("path") else text
96
+ ok = str(target) == str(a.get("value"))
97
+ detail = f"{target!r} {'==' if ok else '!='} {a.get('value')!r}"
98
+ elif t == "regex":
99
+ ok = _regex_search(a.get("value", ""), text) is not None
100
+ detail = f"/{a.get('value')}/ {'matched' if ok else 'no match'}"
101
+ elif t == "json_path":
102
+ val = get_path(output, a.get("path", ""))
103
+ if a.get("value", "") != "":
104
+ ok = str(val) == str(a["value"]); detail = f"{a.get('path')} = {val!r}"
105
+ else:
106
+ ok = val is not None; detail = f"{a.get('path')} {'exists' if ok else 'missing'} (={val!r})"
107
+ elif t == "json_schema":
108
+ schema = a.get("schema") if isinstance(a.get("schema"), dict) else json.loads(a.get("schema") or "{}")
109
+ try:
110
+ js_validate(output, schema); ok = True; detail = "matches schema"
111
+ except ValidationError as ve:
112
+ ok = False; detail = str(ve.message)[:140]
113
+ elif t == "tool_called":
114
+ # Match structured fields only — substring-matching the raw event JSON
115
+ # passed whenever the name appeared anywhere (e.g. inside prompt text).
116
+ name = a.get("value", "")
117
+ called = {meta.get("tool")}
118
+ for e in events:
119
+ if isinstance(e, dict):
120
+ called.update(str(e[k]) for k in ("tool", "tool_name", "name") if e.get(k))
121
+ for tc in e.get("tool_calls") or []:
122
+ if isinstance(tc, dict):
123
+ called.add(str(tc.get("name") or (tc.get("function") or {}).get("name")))
124
+ ok = name in {c for c in called if c}
125
+ detail = f"tool '{name}' {'called' if ok else 'not called'}"
126
+ elif t == "latency_lt":
127
+ ok = dur < float(a.get("value", 0)); detail = f"{dur}ms {'<' if ok else '≥'} {a.get('value')}ms"
128
+ elif t == "llm_judge":
129
+ ok, detail = _llm_judge(db, a, text, workspace_id)
130
+ else:
131
+ detail = f"unknown assertion type: {t}"
132
+ except Exception as exc:
133
+ ok, detail = False, f"error: {exc}"[:140]
134
+ out.append({"type": t, "name": a.get("name") or t, "ok": ok, "detail": detail})
135
+ return out