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,92 @@
1
+ """Convert a promptfoo config into .provekit test files.
2
+
3
+ Promptfoo is the de-facto eval format and OpenAI's official migration target after it
4
+ shut down its own hosted Evals — so importing it is the on-ramp for that user base.
5
+ We map the common, faithfully-translatable pieces and REPORT what we skip (never a
6
+ silent drop): unknown providers, code (javascript/python) asserts, etc.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import yaml
11
+
12
+ from . import testfile
13
+
14
+ # promptfoo assert.type -> (our type, field-mapper)
15
+ _CONTAINS = {"contains", "icontains", "contains-any", "contains-all"}
16
+ _EQUALS = {"equals", "is-equals"}
17
+ _JUDGE = {"llm-rubric", "model-graded-closedqa", "model-graded-factuality", "factuality", "g-eval"}
18
+
19
+
20
+ def _map_assert(a: dict) -> tuple[dict | None, str | None]:
21
+ t = a.get("type", "")
22
+ val = a.get("value")
23
+ if t in _CONTAINS:
24
+ return {"type": "contains", "value": val}, None
25
+ if t in _EQUALS:
26
+ return {"type": "equals", "value": val}, None
27
+ if t == "regex":
28
+ return {"type": "regex", "value": val}, None
29
+ if t in ("is-json", "is-valid-json"):
30
+ schema = val if isinstance(val, dict) else {}
31
+ return {"type": "json_schema", "schema": schema}, None
32
+ if t in ("latency", "latency-lt"):
33
+ return {"type": "latency_lt", "value": a.get("threshold", val)}, None
34
+ if t in _JUDGE:
35
+ return {"type": "llm_judge", "criteria": val}, None
36
+ return None, f"unsupported assert type '{t}'"
37
+
38
+
39
+ def _provider_to_request(provider) -> tuple[dict, str | None]:
40
+ """Return (partial request, connection_name). Handles 'openai:gpt-4o' shorthand and
41
+ the http provider; falls back to a prompt with the raw provider id as the connection."""
42
+ if isinstance(provider, str):
43
+ label, _, model = provider.partition(":")
44
+ return {"type": "prompt", "model": model or label}, label
45
+ if isinstance(provider, dict):
46
+ pid = provider.get("id", "")
47
+ cfg = provider.get("config") or {}
48
+ if pid in ("http", "https") or cfg.get("url"):
49
+ return {"type": "agent", "method": cfg.get("method", "POST"),
50
+ "path": "", "body": cfg.get("body")}, pid or "http"
51
+ label, _, model = pid.partition(":")
52
+ return {"type": "prompt", "model": model or label}, label
53
+ return {"type": "prompt", "model": ""}, None
54
+
55
+
56
+ def import_promptfoo(text: str) -> tuple[list[tuple[str, str]], list[str]]:
57
+ """Returns (files, warnings) where files = [(filename, yaml_text), ...]."""
58
+ cfg = yaml.safe_load(text) or {}
59
+ providers = cfg.get("providers") or [None]
60
+ prompts = cfg.get("prompts") or ["{{prompt}}"]
61
+ tests = cfg.get("tests") or []
62
+ warnings: list[str] = []
63
+
64
+ req_base, conn = _provider_to_request(providers[0])
65
+ if len(providers) > 1:
66
+ warnings.append(f"{len(providers)} providers found — importing only the first ({conn})")
67
+ user_prompt = prompts[0] if isinstance(prompts[0], str) else "{{prompt}}"
68
+ if len(prompts) > 1:
69
+ warnings.append(f"{len(prompts)} prompts found — importing only the first")
70
+
71
+ files: list[tuple[str, str]] = []
72
+ for i, test in enumerate(tests):
73
+ asserts, skipped = [], 0
74
+ for a in test.get("assert") or []:
75
+ mapped, warn = _map_assert(a)
76
+ if mapped:
77
+ asserts.append(mapped)
78
+ else:
79
+ skipped += 1
80
+ warnings.append(f"test {i + 1}: {warn}")
81
+ request = dict(req_base)
82
+ if request["type"] == "prompt":
83
+ request["user"] = user_prompt
84
+ request["assertions"] = asserts
85
+ name = test.get("description") or f"promptfoo test {i + 1}"
86
+ dataset = [{"name": name, "variables": test.get("vars") or {}}] if test.get("vars") else None
87
+ yaml_text = testfile.dump_test(name, request, conn, dataset)
88
+ files.append((f"promptfoo-{i + 1}.yaml", yaml_text))
89
+
90
+ if not tests:
91
+ warnings.append("no tests found in the promptfoo config")
92
+ return files, warnings
File without changes
@@ -0,0 +1,110 @@
1
+ """A2A (Agent2Agent) client — talk to any agent that speaks the Linux Foundation A2A
2
+ protocol. A2A hit v1.0 with 150+ orgs (AWS/Microsoft/ServiceNow/Salesforce) but the
3
+ official inspector is hobby-grade, so a real client is open ground.
4
+
5
+ Supported: agent-card discovery (v1.0 /.well-known/agent-card.json AND the v0.3
6
+ /.well-known/agent.json path), JSON-RPC 2.0 message/send (+ best-effort message/stream).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import uuid
12
+
13
+ import httpx
14
+
15
+ from ..netguard import guard_url
16
+
17
+ _CARD_PATHS = ["/.well-known/agent-card.json", "/.well-known/agent.json"] # v1.0, then v0.3
18
+
19
+
20
+ async def fetch_card(base_url: str, headers: dict | None = None, timeout: float = 15) -> dict:
21
+ """Fetch + lightly validate an agent card. Tries the v1.0 path then the v0.3 path."""
22
+ base = base_url.rstrip("/")
23
+ guard_url(base)
24
+ last = None
25
+ async with httpx.AsyncClient(timeout=timeout, follow_redirects=False) as client:
26
+ for path in _CARD_PATHS:
27
+ try:
28
+ r = await client.get(base + path, headers=headers or None)
29
+ if r.status_code == 200:
30
+ card = r.json()
31
+ if not isinstance(card, dict) or not card.get("name"):
32
+ raise ValueError("agent card missing 'name'")
33
+ card["_path"] = path
34
+ card["_version"] = card.get("protocolVersion") or card.get("version") or "unknown"
35
+ return card
36
+ last = f"HTTP {r.status_code}"
37
+ except Exception as exc: # try the next path
38
+ last = str(exc)
39
+ raise RuntimeError(f"no agent card at {base} ({last})")
40
+
41
+
42
+ def _extract_text(result: dict) -> str:
43
+ """Pull text out of an A2A Message or Task result (parts / artifacts / status)."""
44
+ def parts_text(parts):
45
+ return "".join(p.get("text", "") for p in (parts or []) if p.get("kind", "text") == "text")
46
+
47
+ if not isinstance(result, dict):
48
+ return ""
49
+ if result.get("parts"): # a Message
50
+ return parts_text(result["parts"])
51
+ text = ""
52
+ for art in result.get("artifacts") or []: # a Task's artifacts
53
+ text += parts_text(art.get("parts"))
54
+ if text:
55
+ return text
56
+ status = result.get("status") or {}
57
+ return parts_text((status.get("message") or {}).get("parts"))
58
+
59
+
60
+ def _endpoint(base_url: str, card: dict | None) -> str:
61
+ # A2A v1.0 puts the JSON-RPC endpoint in the card's `url`.
62
+ return (card or {}).get("url") or base_url.rstrip("/")
63
+
64
+
65
+ async def arun(*, base_url: str, text: str, headers: dict | None = None, stream: bool = False,
66
+ card: dict | None = None, timeout: float = 120):
67
+ """Send one message to an A2A agent; yield unified events (delta/result)."""
68
+ endpoint = _endpoint(base_url, card)
69
+ guard_url(endpoint)
70
+ message = {"messageId": uuid.uuid4().hex, "role": "user",
71
+ "parts": [{"kind": "text", "text": text}]}
72
+ method = "message/stream" if stream else "message/send"
73
+ payload = {"jsonrpc": "2.0", "id": uuid.uuid4().hex, "method": method,
74
+ "params": {"message": message}}
75
+ hdrs = {"Content-Type": "application/json", **(headers or {})}
76
+
77
+ # follow_redirects stays False: guard_url only vetted the initial endpoint (SSRF).
78
+ async with httpx.AsyncClient(timeout=timeout, follow_redirects=False) as client:
79
+ if stream:
80
+ collected = ""
81
+ async with client.stream("POST", endpoint, json=payload, headers=hdrs) as resp:
82
+ if resp.status_code >= 400:
83
+ raise RuntimeError(f"A2A error {resp.status_code}: {(await resp.aread()).decode()[:300]}")
84
+ async for line in resp.aiter_lines():
85
+ if not line or not line.startswith("data:"):
86
+ continue
87
+ try:
88
+ evt = json.loads(line[5:].strip())
89
+ except json.JSONDecodeError:
90
+ continue
91
+ result = evt.get("result", evt)
92
+ chunk = _extract_text(result)
93
+ if chunk:
94
+ collected += chunk
95
+ yield {"type": "delta", "text": chunk}
96
+ yield {"type": "result", "data": {"text": collected} if collected else None,
97
+ "meta": {"protocol": "a2a", "streamed": True}}
98
+ return
99
+
100
+ resp = await client.post(endpoint, json=payload, headers=hdrs)
101
+ if resp.status_code >= 400:
102
+ raise RuntimeError(f"A2A error {resp.status_code}: {resp.text[:300]}")
103
+ data = resp.json()
104
+ if "error" in data:
105
+ raise RuntimeError(str((data["error"] or {}).get("message", "A2A error")))
106
+ result = data.get("result", data)
107
+ text_out = _extract_text(result)
108
+ if text_out:
109
+ yield {"type": "delta", "text": text_out}
110
+ yield {"type": "result", "data": result, "meta": {"protocol": "a2a"}}
@@ -0,0 +1,55 @@
1
+ """Generic HTTP agent caller (async) — call any agent/API endpoint, with optional SSE
2
+ streaming. Yields {"type":"delta"} for streamed text/events and {"type":"result"} for a
3
+ buffered JSON/text response."""
4
+ from __future__ import annotations
5
+
6
+ import json
7
+
8
+ import httpx
9
+
10
+ from ..netguard import guard_url
11
+
12
+ # Cap a buffered agent response so a hostile/misbehaving endpoint can't OOM the worker.
13
+ MAX_RESPONSE_BYTES = 16 * 1024 * 1024
14
+
15
+
16
+ async def arun(*, base_url: str, method: str, path: str, headers: dict | None = None,
17
+ body=None, stream: bool = False, timeout: float = 120):
18
+ url = base_url.rstrip("/") + "/" + path.lstrip("/") if path else base_url.rstrip("/")
19
+ guard_url(url)
20
+ method = (method or "POST").upper()
21
+ hdrs = {"Accept": "application/json, text/event-stream", **(headers or {})}
22
+
23
+ # follow_redirects stays False (httpx default) on purpose: guard_url only vetted the
24
+ # initial URL, so a redirect could point at an internal address (SSRF).
25
+ async with httpx.AsyncClient(timeout=timeout, follow_redirects=False) as client:
26
+ if stream:
27
+ async with client.stream(method, url, json=body if body is not None else None, headers=hdrs) as resp:
28
+ status = resp.status_code
29
+ async for line in resp.aiter_lines():
30
+ if not line:
31
+ continue
32
+ if line.startswith("data:"):
33
+ data = line[5:].strip()
34
+ if data == "[DONE]":
35
+ break
36
+ try:
37
+ yield {"type": "event", "data": json.loads(data)}
38
+ except json.JSONDecodeError:
39
+ yield {"type": "delta", "text": data}
40
+ else:
41
+ yield {"type": "delta", "text": line}
42
+ yield {"type": "result", "data": None, "meta": {"status": status, "streamed": True}}
43
+ return
44
+
45
+ async with client.stream(method, url, json=body if body is not None else None, headers=hdrs) as resp:
46
+ status = resp.status_code
47
+ ctype = resp.headers.get("content-type", "")
48
+ buf = bytearray()
49
+ async for chunk in resp.aiter_bytes():
50
+ buf += chunk
51
+ if len(buf) > MAX_RESPONSE_BYTES:
52
+ raise ValueError(f"agent response exceeded {MAX_RESPONSE_BYTES} bytes")
53
+ raw = bytes(buf)
54
+ data = json.loads(raw) if ctype.startswith("application/json") and raw else raw.decode("utf-8", "replace")
55
+ yield {"type": "result", "data": data, "meta": {"status": status}}
@@ -0,0 +1,364 @@
1
+ """Generic LLM streaming over httpx.AsyncClient — provider-agnostic (OpenAI, Anthropic,
2
+ any OpenAI-compatible base URL, OpenAI Responses / Open Responses).
3
+
4
+ `astream(...)` is an async generator yielding {"type":"delta"} tokens, optional
5
+ {"type":"node"} stop events, {"type":"tool_call"} when the model asks for a tool, and a
6
+ final {"type":"usage"}. Async so a long stream occupies the event loop, not a threadpool
7
+ thread.
8
+
9
+ Messages are provider-agnostic; each provider translates them on the way out:
10
+
11
+ {"role": "user", "content": str}
12
+ {"role": "assistant", "content": str, "tool_calls": [{"id", "name", "args"}]}
13
+ {"role": "tool", "tool_call_id": str, "name": str, "content": str}
14
+
15
+ The caller (dispatch) owns the tool loop — it appends the assistant/tool turns above and
16
+ calls astream again. Providers disagree on how a tool round is represented, which is
17
+ exactly why that shape is normalized here rather than leaking into the dispatcher.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import asyncio
22
+ import json
23
+
24
+ import httpx
25
+
26
+ from ..netguard import guard_url
27
+
28
+
29
+ def _mock_reply(user: str, system: str | None) -> str:
30
+ """Canned but context-aware reply for the keyless demo agent."""
31
+ u = (user or "").lower()
32
+ sys = (system or "").lower()
33
+ if not user:
34
+ return "Hi — I'm the ProveKit demo agent. Ask me anything. I run with no API key, so you can try the console instantly."
35
+ if "classify" in sys or "classify" in u:
36
+ return "support_request"
37
+ if "extract" in sys or "json" in sys:
38
+ return '{"summary": "demo extraction", "priority": "normal", "is_demo": true}'
39
+ if "agent" in u and any(k in u for k in ("what", "explain", "define")):
40
+ return ("An AI agent is a program that reads context, decides on an action, and uses tools or model "
41
+ "calls to reach a goal — often looping until the task is done. This reply came from ProveKit's "
42
+ "built-in mock provider, so no API key was needed.")
43
+ if any(k in u for k in ("urgent", "angry", "complaint", "refund", "cancel", "asap")):
44
+ return "This looks urgent — it mentions a time-sensitive or negative signal, so I'd route it to a human. (Demo mock response.)"
45
+ short = " ".join((user or "").split())[:160]
46
+ return (f"Here's a demo answer to: \"{short}\". In a live run this would come from your OpenAI or Anthropic "
47
+ "connection — swap one in from the Connections tab. For now this is ProveKit's keyless mock agent.")
48
+
49
+
50
+ def _mock_tool_args(schema: dict, user: str) -> dict:
51
+ """Fill a tool's required string params with the user's text — enough for the keyless
52
+ demo to make a real, plausible MCP call."""
53
+ props = (schema or {}).get("properties") or {}
54
+ args = {}
55
+ for name in (schema or {}).get("required") or []:
56
+ spec = props.get(name) or {}
57
+ t = spec.get("type", "string")
58
+ if t == "string":
59
+ args[name] = " ".join((user or "demo").split())[:80]
60
+ elif t in ("number", "integer"):
61
+ args[name] = 1
62
+ elif t == "boolean":
63
+ args[name] = True
64
+ return args
65
+
66
+
67
+ async def _stream_mock(system, messages, tools=None):
68
+ """Stream a canned reply token-by-token — a working agent with no credentials.
69
+
70
+ With tools attached it calls the first one once, then answers using the result, so the
71
+ whole MCP tool-calling loop is demonstrable offline with no API key.
72
+ """
73
+ user = next((str(m.get("content") or "") for m in reversed(messages) if m.get("role") == "user"), "")
74
+ already_ran = any(m.get("role") == "tool" for m in messages)
75
+ if tools and not already_ran:
76
+ t = tools[0]
77
+ # the three provider tool schemas nest the name/params differently
78
+ fn = t.get("function") or t
79
+ schema = fn.get("parameters") or fn.get("input_schema") or {}
80
+ yield {"type": "tool_call", "call": {"id": "mock-call-1", "name": fn.get("name", ""),
81
+ "args": _mock_tool_args(schema, user)}}
82
+ return
83
+ if already_ran:
84
+ out = next((str(m.get("content") or "") for m in reversed(messages) if m.get("role") == "tool"), "")
85
+ reply = (f"The tool returned: {' '.join(out.split())[:200]}. "
86
+ "(Demo mock agent — it called a real MCP tool, with no API key.)")
87
+ else:
88
+ reply = _mock_reply(user, system)
89
+ spent = 0.0 # cap total artificial delay so concurrent demo runs stay cheap
90
+ for tok in reply.split(" "):
91
+ yield {"type": "delta", "text": tok + " "}
92
+ if spent < 0.6:
93
+ await asyncio.sleep(0.012); spent += 0.012
94
+ ptok = sum(len(str(m.get("content") or "").split()) for m in messages) + len((system or "").split())
95
+ ctok = len(reply.split())
96
+ yield {"type": "usage", "usage": {"prompt_tokens": ptok, "completion_tokens": ctok, "total_tokens": ptok + ctok}}
97
+
98
+
99
+ DEFAULT_BASE = {
100
+ "openai": "https://api.openai.com/v1",
101
+ "openai-responses": "https://api.openai.com/v1",
102
+ "compatible": "http://localhost:11434/v1",
103
+ "anthropic": "https://api.anthropic.com/v1",
104
+ }
105
+
106
+
107
+ # Some 2026 models reject sampling params — send temperature only when it's meaningful.
108
+ def _maybe_temp(body: dict, temperature: float) -> None:
109
+ if temperature is not None:
110
+ body["temperature"] = temperature
111
+
112
+
113
+ async def _aiter_sse(resp: httpx.Response):
114
+ """Yield parsed `data:` JSON objects from an SSE response (skips [DONE])."""
115
+ async for line in resp.aiter_lines():
116
+ if not line or not line.startswith("data:"):
117
+ continue
118
+ data = line[5:].strip()
119
+ if data == "[DONE]":
120
+ return
121
+ try:
122
+ yield json.loads(data)
123
+ except json.JSONDecodeError:
124
+ continue
125
+
126
+
127
+ async def astream(*, provider: str, base_url: str, api_key: str, model: str,
128
+ system: str | None, messages: list[dict], temperature: float = 0.7,
129
+ max_tokens: int = 1024, tools: list[dict] | None = None):
130
+ provider = (provider or "openai").lower()
131
+ base = (base_url or DEFAULT_BASE.get(provider) or DEFAULT_BASE["openai"]).rstrip("/")
132
+
133
+ if provider == "mock":
134
+ async for ev in _stream_mock(system, messages, tools):
135
+ yield ev
136
+ return
137
+ guard_url(base)
138
+ if provider == "anthropic":
139
+ gen = _stream_anthropic(base, api_key, model, system, messages, temperature, max_tokens, tools)
140
+ elif provider == "openai-responses":
141
+ gen = _stream_responses(base, api_key, model, system, messages, temperature, max_tokens, tools)
142
+ else:
143
+ gen = _stream_openai(base, api_key, model, system, messages, temperature, max_tokens, tools)
144
+ async for ev in gen:
145
+ yield ev
146
+
147
+
148
+ def _args(raw: str) -> dict:
149
+ """Tool arguments arrive as a JSON string, streamed in fragments. A model can emit
150
+ invalid JSON; that's a failed tool call, not a failed run, so it degrades to {}."""
151
+ try:
152
+ out = json.loads(raw or "{}")
153
+ return out if isinstance(out, dict) else {}
154
+ except json.JSONDecodeError:
155
+ return {}
156
+
157
+
158
+ def collect_text_sync(**kwargs) -> str:
159
+ """Drive astream to completion and return the concatenated text — for sync callers
160
+ (the LLM-judge assertion, the CLI). Runs its own event loop; never call from within one."""
161
+ async def _run():
162
+ parts = []
163
+ async for ev in astream(**kwargs):
164
+ if ev["type"] == "delta":
165
+ parts.append(ev["text"])
166
+ return "".join(parts)
167
+ return asyncio.run(_run())
168
+
169
+
170
+ def _openai_messages(messages: list[dict]) -> list[dict]:
171
+ out = []
172
+ for m in messages:
173
+ if m.get("role") == "tool":
174
+ out.append({"role": "tool", "tool_call_id": m.get("tool_call_id"),
175
+ "content": m.get("content", "")})
176
+ elif m.get("tool_calls"):
177
+ out.append({"role": "assistant", "content": m.get("content") or None,
178
+ "tool_calls": [{"id": c["id"], "type": "function",
179
+ "function": {"name": c["name"],
180
+ "arguments": json.dumps(c.get("args") or {})}}
181
+ for c in m["tool_calls"]]})
182
+ else:
183
+ out.append({"role": m.get("role", "user"), "content": m.get("content", "")})
184
+ return out
185
+
186
+
187
+ async def _stream_openai(base, api_key, model, system, messages, temperature, max_tokens, tools=None):
188
+ msgs = ([{"role": "system", "content": system}] if system else []) + _openai_messages(messages)
189
+ body = {"model": model, "messages": msgs, "stream": True,
190
+ "temperature": temperature, "max_tokens": max_tokens,
191
+ "stream_options": {"include_usage": True}}
192
+ if tools:
193
+ body["tools"] = tools
194
+ headers = {"Content-Type": "application/json"}
195
+ if api_key:
196
+ headers["Authorization"] = f"Bearer {api_key}"
197
+ async with httpx.AsyncClient(timeout=120, follow_redirects=False) as client:
198
+ async with client.stream("POST", f"{base}/chat/completions", json=body, headers=headers) as resp:
199
+ if resp.status_code >= 400:
200
+ raise RuntimeError(f"LLM error {resp.status_code}: {(await resp.aread()).decode()[:400]}")
201
+ # tool_calls stream in fragments keyed by index: the id and name land on the
202
+ # first chunk, then `arguments` arrives a few characters at a time.
203
+ pending: dict[int, dict] = {}
204
+ async for obj in _aiter_sse(resp):
205
+ for choice in obj.get("choices", []):
206
+ delta = choice.get("delta") or {}
207
+ if delta.get("content"):
208
+ yield {"type": "delta", "text": delta["content"]}
209
+ for tc in delta.get("tool_calls") or []:
210
+ slot = pending.setdefault(tc.get("index", 0), {"id": "", "name": "", "raw": ""})
211
+ if tc.get("id"):
212
+ slot["id"] = tc["id"]
213
+ fn = tc.get("function") or {}
214
+ if fn.get("name"):
215
+ slot["name"] = fn["name"]
216
+ if fn.get("arguments"):
217
+ slot["raw"] += fn["arguments"]
218
+ if obj.get("usage"):
219
+ yield {"type": "usage", "usage": obj["usage"]}
220
+ for slot in pending.values():
221
+ if slot["name"]:
222
+ yield {"type": "tool_call",
223
+ "call": {"id": slot["id"], "name": slot["name"], "args": _args(slot["raw"])}}
224
+
225
+
226
+ def _responses_input(messages: list[dict]) -> list[dict]:
227
+ """Responses takes a flat item list: a tool round is a function_call item followed by a
228
+ matching function_call_output item, not assistant/tool roles."""
229
+ items = []
230
+ for m in messages:
231
+ if m.get("role") == "tool":
232
+ items.append({"type": "function_call_output", "call_id": m.get("tool_call_id"),
233
+ "output": m.get("content", "")})
234
+ continue
235
+ if m.get("content"):
236
+ items.append({"role": "assistant" if m.get("role") == "assistant" else "user",
237
+ "content": m["content"]})
238
+ for c in m.get("tool_calls") or []:
239
+ items.append({"type": "function_call", "call_id": c["id"], "name": c["name"],
240
+ "arguments": json.dumps(c.get("args") or {})})
241
+ return items
242
+
243
+
244
+ async def _stream_responses(base, api_key, model, system, messages, temperature, max_tokens, tools=None):
245
+ """OpenAI Responses API / 'Open Responses' (also vLLM, Ollama, HF, OpenRouter)."""
246
+ body = {"model": model, "input": _responses_input(messages), "stream": True,
247
+ "max_output_tokens": max_tokens}
248
+ if tools:
249
+ body["tools"] = tools
250
+ if system:
251
+ body["instructions"] = system
252
+ _maybe_temp(body, temperature)
253
+ headers = {"Content-Type": "application/json"}
254
+ if api_key:
255
+ headers["Authorization"] = f"Bearer {api_key}"
256
+ async with httpx.AsyncClient(timeout=120, follow_redirects=False) as client:
257
+ async with client.stream("POST", f"{base}/responses", json=body, headers=headers) as resp:
258
+ if resp.status_code >= 400:
259
+ raise RuntimeError(f"LLM error {resp.status_code}: {(await resp.aread()).decode()[:400]}")
260
+ # `.added` announces the call with EMPTY arguments; `.done` carries the filled-in
261
+ # ones. Both are kept in one slot per call_id so the later, fuller payload wins —
262
+ # taking the first and deduping would report every call with no arguments at all.
263
+ pending: dict[str, dict] = {}
264
+ async for obj in _aiter_sse(resp):
265
+ et = obj.get("type", "")
266
+ if et == "response.output_text.delta" and obj.get("delta"):
267
+ yield {"type": "delta", "text": obj["delta"]}
268
+ elif et in ("response.output_item.added", "response.output_item.done"):
269
+ item = obj.get("item") or {}
270
+ if item.get("type") != "function_call":
271
+ continue
272
+ cid = item.get("call_id") or item.get("id") or ""
273
+ slot = pending.setdefault(cid, {"name": "", "raw": ""})
274
+ if item.get("name"):
275
+ slot["name"] = item["name"]
276
+ if item.get("arguments"):
277
+ slot["raw"] = item["arguments"]
278
+ if et.endswith(".done") and slot["name"]:
279
+ yield {"type": "tool_call",
280
+ "call": {"id": cid, "name": slot["name"], "args": _args(slot["raw"])}}
281
+ pending.pop(cid, None)
282
+ elif et in ("response.completed", "response.incomplete"):
283
+ usage = (obj.get("response") or {}).get("usage")
284
+ if usage:
285
+ yield {"type": "usage", "usage": usage}
286
+ for cid, slot in pending.items():
287
+ if slot["name"]: # announced but never completed — report it rather than lose it
288
+ yield {"type": "tool_call",
289
+ "call": {"id": cid, "name": slot["name"], "args": _args(slot["raw"])}}
290
+
291
+
292
+ def _anthropic_messages(messages: list[dict]) -> list[dict]:
293
+ """Anthropic carries a tool round as content blocks: tool_use on the assistant turn, and
294
+ tool_result blocks on the *user* turn. Consecutive results must be merged into one user
295
+ message — a separate message per result is rejected."""
296
+ out: list[dict] = []
297
+ for m in messages:
298
+ role = m.get("role")
299
+ if role == "tool":
300
+ block = {"type": "tool_result", "tool_use_id": m.get("tool_call_id"),
301
+ "content": m.get("content", "")}
302
+ if out and out[-1]["role"] == "user" and isinstance(out[-1]["content"], list):
303
+ out[-1]["content"].append(block)
304
+ else:
305
+ out.append({"role": "user", "content": [block]})
306
+ elif m.get("tool_calls"):
307
+ blocks = ([{"type": "text", "text": m["content"]}] if m.get("content") else [])
308
+ blocks += [{"type": "tool_use", "id": c["id"], "name": c["name"],
309
+ "input": c.get("args") or {}} for c in m["tool_calls"]]
310
+ out.append({"role": "assistant", "content": blocks})
311
+ elif role in ("user", "assistant"):
312
+ out.append({"role": role, "content": m.get("content", "")})
313
+ return out
314
+
315
+
316
+ async def _stream_anthropic(base, api_key, model, system, messages, temperature, max_tokens, tools=None):
317
+ body = {"model": model, "messages": _anthropic_messages(messages), "stream": True,
318
+ "max_tokens": max_tokens}
319
+ if tools:
320
+ body["tools"] = tools
321
+ _maybe_temp(body, temperature)
322
+ if system:
323
+ body["system"] = system
324
+ headers = {"Content-Type": "application/json", "anthropic-version": "2023-06-01"}
325
+ if api_key:
326
+ headers["x-api-key"] = api_key
327
+ async with httpx.AsyncClient(timeout=120, follow_redirects=False) as client:
328
+ async with client.stream("POST", f"{base}/messages", json=body, headers=headers) as resp:
329
+ if resp.status_code >= 400:
330
+ raise RuntimeError(f"LLM error {resp.status_code}: {(await resp.aread()).decode()[:400]}")
331
+ usage = {}
332
+ cur: dict | None = None # the tool_use block being streamed, if any
333
+ async for obj in _aiter_sse(resp):
334
+ t = obj.get("type")
335
+ if t == "content_block_start":
336
+ blk = obj.get("content_block") or {}
337
+ if blk.get("type") == "tool_use":
338
+ cur = {"id": blk.get("id", ""), "name": blk.get("name", ""), "raw": ""}
339
+ elif t == "content_block_delta":
340
+ d = obj.get("delta") or {}
341
+ if d.get("type") == "text_delta" and d.get("text"):
342
+ yield {"type": "delta", "text": d["text"]}
343
+ elif d.get("type") == "input_json_delta" and cur is not None:
344
+ # the tool's input object arrives as JSON text fragments
345
+ cur["raw"] += d.get("partial_json") or ""
346
+ elif t == "content_block_stop" and cur is not None:
347
+ yield {"type": "tool_call",
348
+ "call": {"id": cur["id"], "name": cur["name"], "args": _args(cur["raw"])}}
349
+ cur = None
350
+ elif t == "message_start":
351
+ usage = (obj.get("message") or {}).get("usage", {}) or usage
352
+ elif t == "message_delta":
353
+ if obj.get("usage"):
354
+ usage = {**usage, **obj["usage"]}
355
+ stop = (obj.get("delta") or {}).get("stop_reason")
356
+ if stop and stop not in ("end_turn", "stop_sequence"):
357
+ yield {"type": "node", "data": {"stop_reason": stop}}
358
+ if cur is not None:
359
+ # Stream ended without a content_block_stop (truncated/aborted): still report
360
+ # the call rather than losing it.
361
+ yield {"type": "tool_call",
362
+ "call": {"id": cur["id"], "name": cur["name"], "args": _args(cur["raw"])}}
363
+ if usage:
364
+ yield {"type": "usage", "usage": usage}