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.
- provekit/__init__.py +0 -0
- provekit/cli.py +256 -0
- provekit/config.py +77 -0
- provekit/database.py +132 -0
- provekit/main.py +115 -0
- provekit/migrations/env.py +40 -0
- provekit/migrations/script.py.mako +22 -0
- provekit/migrations/versions/323eb73d463c_user_email_verified.py +32 -0
- provekit/migrations/versions/49e8ab812556_baseline_schema.py +241 -0
- provekit/migrations/versions/819ed5ff183e_workspace_ingest_key.py +35 -0
- provekit/migrations/versions/a1b2c3d4e5f6_user_token_version.py +27 -0
- provekit/models.py +191 -0
- provekit/observability.py +154 -0
- provekit/routers/__init__.py +0 -0
- provekit/routers/auth.py +150 -0
- provekit/routers/connections.py +270 -0
- provekit/routers/deployments.py +161 -0
- provekit/routers/flows.py +188 -0
- provekit/routers/library.py +265 -0
- provekit/routers/prompts.py +72 -0
- provekit/routers/run.py +203 -0
- provekit/routers/runtime.py +103 -0
- provekit/routers/traces.py +65 -0
- provekit/routers/usage.py +42 -0
- provekit/services/__init__.py +0 -0
- provekit/services/assertions.py +135 -0
- provekit/services/auth.py +131 -0
- provekit/services/deploy.py +63 -0
- provekit/services/dispatch.py +313 -0
- provekit/services/email.py +38 -0
- provekit/services/flow.py +299 -0
- provekit/services/limits.py +109 -0
- provekit/services/masking.py +45 -0
- provekit/services/netguard.py +69 -0
- provekit/services/otel.py +146 -0
- provekit/services/promptfoo.py +92 -0
- provekit/services/providers/__init__.py +0 -0
- provekit/services/providers/a2a_client.py +110 -0
- provekit/services/providers/agent_http.py +55 -0
- provekit/services/providers/llm.py +364 -0
- provekit/services/providers/mcp_client.py +289 -0
- provekit/services/runstore.py +93 -0
- provekit/services/sealing.py +106 -0
- provekit/services/templates.py +69 -0
- provekit/services/testfile.py +122 -0
- provekit/services/tooling.py +194 -0
- provekit/services/workspace.py +91 -0
- provekit/templates/flows/_manifest.json +2594 -0
- provekit/templates/flows/banking-action-items.yaml +43 -0
- provekit/templates/flows/banking-answer.yaml +43 -0
- provekit/templates/flows/banking-classify-intent.yaml +43 -0
- provekit/templates/flows/banking-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/banking-extract-to-json.yaml +43 -0
- provekit/templates/flows/banking-faq-match-answer.yaml +60 -0
- provekit/templates/flows/banking-keyword-extract.yaml +43 -0
- provekit/templates/flows/banking-language-route.yaml +103 -0
- provekit/templates/flows/banking-moderation-gate.yaml +73 -0
- provekit/templates/flows/banking-redact-summarize.yaml +59 -0
- provekit/templates/flows/banking-score-gate.yaml +73 -0
- provekit/templates/flows/banking-sentiment-route.yaml +73 -0
- provekit/templates/flows/banking-severity-tiers.yaml +103 -0
- provekit/templates/flows/banking-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/banking-tag-enrich.yaml +43 -0
- provekit/templates/flows/banking-tone-rewrite.yaml +43 -0
- provekit/templates/flows/banking-translate-reply.yaml +58 -0
- provekit/templates/flows/banking-triage-route.yaml +73 -0
- provekit/templates/flows/community-forum-action-items.yaml +43 -0
- provekit/templates/flows/community-forum-answer.yaml +43 -0
- provekit/templates/flows/community-forum-classify-intent.yaml +43 -0
- provekit/templates/flows/community-forum-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/community-forum-extract-to-json.yaml +43 -0
- provekit/templates/flows/community-forum-faq-match-answer.yaml +60 -0
- provekit/templates/flows/community-forum-keyword-extract.yaml +43 -0
- provekit/templates/flows/community-forum-language-route.yaml +103 -0
- provekit/templates/flows/community-forum-moderation-gate.yaml +73 -0
- provekit/templates/flows/community-forum-redact-summarize.yaml +59 -0
- provekit/templates/flows/community-forum-score-gate.yaml +73 -0
- provekit/templates/flows/community-forum-sentiment-route.yaml +73 -0
- provekit/templates/flows/community-forum-severity-tiers.yaml +103 -0
- provekit/templates/flows/community-forum-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/community-forum-tag-enrich.yaml +43 -0
- provekit/templates/flows/community-forum-tone-rewrite.yaml +43 -0
- provekit/templates/flows/community-forum-translate-reply.yaml +58 -0
- provekit/templates/flows/community-forum-triage-route.yaml +73 -0
- provekit/templates/flows/content-moderation-action-items.yaml +43 -0
- provekit/templates/flows/content-moderation-answer.yaml +43 -0
- provekit/templates/flows/content-moderation-classify-intent.yaml +43 -0
- provekit/templates/flows/content-moderation-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/content-moderation-extract-to-json.yaml +43 -0
- provekit/templates/flows/content-moderation-faq-match-answer.yaml +60 -0
- provekit/templates/flows/content-moderation-keyword-extract.yaml +43 -0
- provekit/templates/flows/content-moderation-language-route.yaml +103 -0
- provekit/templates/flows/content-moderation-moderation-gate.yaml +73 -0
- provekit/templates/flows/content-moderation-redact-summarize.yaml +59 -0
- provekit/templates/flows/content-moderation-score-gate.yaml +73 -0
- provekit/templates/flows/content-moderation-sentiment-route.yaml +73 -0
- provekit/templates/flows/content-moderation-severity-tiers.yaml +103 -0
- provekit/templates/flows/content-moderation-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/content-moderation-tag-enrich.yaml +43 -0
- provekit/templates/flows/content-moderation-tone-rewrite.yaml +43 -0
- provekit/templates/flows/content-moderation-translate-reply.yaml +58 -0
- provekit/templates/flows/content-moderation-triage-route.yaml +73 -0
- provekit/templates/flows/customer-support-action-items.yaml +43 -0
- provekit/templates/flows/customer-support-answer.yaml +43 -0
- provekit/templates/flows/customer-support-classify-intent.yaml +43 -0
- provekit/templates/flows/customer-support-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/customer-support-extract-to-json.yaml +43 -0
- provekit/templates/flows/customer-support-faq-match-answer.yaml +60 -0
- provekit/templates/flows/customer-support-keyword-extract.yaml +43 -0
- provekit/templates/flows/customer-support-language-route.yaml +103 -0
- provekit/templates/flows/customer-support-moderation-gate.yaml +73 -0
- provekit/templates/flows/customer-support-redact-summarize.yaml +59 -0
- provekit/templates/flows/customer-support-score-gate.yaml +73 -0
- provekit/templates/flows/customer-support-sentiment-route.yaml +73 -0
- provekit/templates/flows/customer-support-severity-tiers.yaml +103 -0
- provekit/templates/flows/customer-support-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/customer-support-tag-enrich.yaml +43 -0
- provekit/templates/flows/customer-support-tone-rewrite.yaml +43 -0
- provekit/templates/flows/customer-support-translate-reply.yaml +58 -0
- provekit/templates/flows/customer-support-triage-route.yaml +73 -0
- provekit/templates/flows/developer-tooling-action-items.yaml +43 -0
- provekit/templates/flows/developer-tooling-answer.yaml +43 -0
- provekit/templates/flows/developer-tooling-classify-intent.yaml +43 -0
- provekit/templates/flows/developer-tooling-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/developer-tooling-extract-to-json.yaml +43 -0
- provekit/templates/flows/developer-tooling-faq-match-answer.yaml +60 -0
- provekit/templates/flows/developer-tooling-keyword-extract.yaml +43 -0
- provekit/templates/flows/developer-tooling-language-route.yaml +103 -0
- provekit/templates/flows/developer-tooling-moderation-gate.yaml +73 -0
- provekit/templates/flows/developer-tooling-redact-summarize.yaml +59 -0
- provekit/templates/flows/developer-tooling-score-gate.yaml +73 -0
- provekit/templates/flows/developer-tooling-sentiment-route.yaml +73 -0
- provekit/templates/flows/developer-tooling-severity-tiers.yaml +103 -0
- provekit/templates/flows/developer-tooling-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/developer-tooling-tag-enrich.yaml +43 -0
- provekit/templates/flows/developer-tooling-tone-rewrite.yaml +43 -0
- provekit/templates/flows/developer-tooling-translate-reply.yaml +58 -0
- provekit/templates/flows/developer-tooling-triage-route.yaml +73 -0
- provekit/templates/flows/devrel-action-items.yaml +43 -0
- provekit/templates/flows/devrel-answer.yaml +43 -0
- provekit/templates/flows/devrel-classify-intent.yaml +43 -0
- provekit/templates/flows/devrel-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/devrel-extract-to-json.yaml +43 -0
- provekit/templates/flows/devrel-faq-match-answer.yaml +60 -0
- provekit/templates/flows/devrel-keyword-extract.yaml +43 -0
- provekit/templates/flows/devrel-language-route.yaml +103 -0
- provekit/templates/flows/devrel-moderation-gate.yaml +73 -0
- provekit/templates/flows/devrel-redact-summarize.yaml +59 -0
- provekit/templates/flows/devrel-score-gate.yaml +73 -0
- provekit/templates/flows/devrel-sentiment-route.yaml +73 -0
- provekit/templates/flows/devrel-severity-tiers.yaml +103 -0
- provekit/templates/flows/devrel-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/devrel-tag-enrich.yaml +43 -0
- provekit/templates/flows/devrel-tone-rewrite.yaml +43 -0
- provekit/templates/flows/devrel-translate-reply.yaml +58 -0
- provekit/templates/flows/devrel-triage-route.yaml +73 -0
- provekit/templates/flows/e-commerce-action-items.yaml +43 -0
- provekit/templates/flows/e-commerce-answer.yaml +43 -0
- provekit/templates/flows/e-commerce-classify-intent.yaml +43 -0
- provekit/templates/flows/e-commerce-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/e-commerce-extract-to-json.yaml +43 -0
- provekit/templates/flows/e-commerce-faq-match-answer.yaml +60 -0
- provekit/templates/flows/e-commerce-keyword-extract.yaml +43 -0
- provekit/templates/flows/e-commerce-language-route.yaml +103 -0
- provekit/templates/flows/e-commerce-moderation-gate.yaml +73 -0
- provekit/templates/flows/e-commerce-redact-summarize.yaml +59 -0
- provekit/templates/flows/e-commerce-score-gate.yaml +73 -0
- provekit/templates/flows/e-commerce-sentiment-route.yaml +73 -0
- provekit/templates/flows/e-commerce-severity-tiers.yaml +103 -0
- provekit/templates/flows/e-commerce-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/e-commerce-tag-enrich.yaml +43 -0
- provekit/templates/flows/e-commerce-tone-rewrite.yaml +43 -0
- provekit/templates/flows/e-commerce-translate-reply.yaml +58 -0
- provekit/templates/flows/e-commerce-triage-route.yaml +73 -0
- provekit/templates/flows/education-action-items.yaml +43 -0
- provekit/templates/flows/education-answer.yaml +43 -0
- provekit/templates/flows/education-classify-intent.yaml +43 -0
- provekit/templates/flows/education-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/education-extract-to-json.yaml +43 -0
- provekit/templates/flows/education-faq-match-answer.yaml +60 -0
- provekit/templates/flows/education-keyword-extract.yaml +43 -0
- provekit/templates/flows/education-language-route.yaml +103 -0
- provekit/templates/flows/education-moderation-gate.yaml +73 -0
- provekit/templates/flows/education-redact-summarize.yaml +59 -0
- provekit/templates/flows/education-score-gate.yaml +73 -0
- provekit/templates/flows/education-sentiment-route.yaml +73 -0
- provekit/templates/flows/education-severity-tiers.yaml +103 -0
- provekit/templates/flows/education-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/education-tag-enrich.yaml +43 -0
- provekit/templates/flows/education-tone-rewrite.yaml +43 -0
- provekit/templates/flows/education-translate-reply.yaml +58 -0
- provekit/templates/flows/education-triage-route.yaml +73 -0
- provekit/templates/flows/finance-action-items.yaml +43 -0
- provekit/templates/flows/finance-answer.yaml +43 -0
- provekit/templates/flows/finance-classify-intent.yaml +43 -0
- provekit/templates/flows/finance-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/finance-extract-to-json.yaml +43 -0
- provekit/templates/flows/finance-faq-match-answer.yaml +60 -0
- provekit/templates/flows/finance-keyword-extract.yaml +43 -0
- provekit/templates/flows/finance-language-route.yaml +103 -0
- provekit/templates/flows/finance-moderation-gate.yaml +73 -0
- provekit/templates/flows/finance-redact-summarize.yaml +59 -0
- provekit/templates/flows/finance-score-gate.yaml +73 -0
- provekit/templates/flows/finance-sentiment-route.yaml +73 -0
- provekit/templates/flows/finance-severity-tiers.yaml +103 -0
- provekit/templates/flows/finance-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/finance-tag-enrich.yaml +43 -0
- provekit/templates/flows/finance-tone-rewrite.yaml +43 -0
- provekit/templates/flows/finance-translate-reply.yaml +58 -0
- provekit/templates/flows/finance-triage-route.yaml +73 -0
- provekit/templates/flows/food-delivery-action-items.yaml +43 -0
- provekit/templates/flows/food-delivery-answer.yaml +43 -0
- provekit/templates/flows/food-delivery-classify-intent.yaml +43 -0
- provekit/templates/flows/food-delivery-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/food-delivery-extract-to-json.yaml +43 -0
- provekit/templates/flows/food-delivery-faq-match-answer.yaml +60 -0
- provekit/templates/flows/food-delivery-keyword-extract.yaml +43 -0
- provekit/templates/flows/food-delivery-language-route.yaml +103 -0
- provekit/templates/flows/food-delivery-moderation-gate.yaml +73 -0
- provekit/templates/flows/food-delivery-redact-summarize.yaml +59 -0
- provekit/templates/flows/food-delivery-score-gate.yaml +73 -0
- provekit/templates/flows/food-delivery-sentiment-route.yaml +73 -0
- provekit/templates/flows/food-delivery-severity-tiers.yaml +103 -0
- provekit/templates/flows/food-delivery-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/food-delivery-tag-enrich.yaml +43 -0
- provekit/templates/flows/food-delivery-tone-rewrite.yaml +43 -0
- provekit/templates/flows/food-delivery-translate-reply.yaml +58 -0
- provekit/templates/flows/food-delivery-triage-route.yaml +73 -0
- provekit/templates/flows/gaming-support-action-items.yaml +43 -0
- provekit/templates/flows/gaming-support-answer.yaml +43 -0
- provekit/templates/flows/gaming-support-classify-intent.yaml +43 -0
- provekit/templates/flows/gaming-support-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/gaming-support-extract-to-json.yaml +43 -0
- provekit/templates/flows/gaming-support-faq-match-answer.yaml +60 -0
- provekit/templates/flows/gaming-support-keyword-extract.yaml +43 -0
- provekit/templates/flows/gaming-support-language-route.yaml +103 -0
- provekit/templates/flows/gaming-support-moderation-gate.yaml +73 -0
- provekit/templates/flows/gaming-support-redact-summarize.yaml +59 -0
- provekit/templates/flows/gaming-support-score-gate.yaml +73 -0
- provekit/templates/flows/gaming-support-sentiment-route.yaml +73 -0
- provekit/templates/flows/gaming-support-severity-tiers.yaml +103 -0
- provekit/templates/flows/gaming-support-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/gaming-support-tag-enrich.yaml +43 -0
- provekit/templates/flows/gaming-support-tone-rewrite.yaml +43 -0
- provekit/templates/flows/gaming-support-translate-reply.yaml +58 -0
- provekit/templates/flows/gaming-support-triage-route.yaml +73 -0
- provekit/templates/flows/healthcare-intake-action-items.yaml +43 -0
- provekit/templates/flows/healthcare-intake-answer.yaml +43 -0
- provekit/templates/flows/healthcare-intake-classify-intent.yaml +43 -0
- provekit/templates/flows/healthcare-intake-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/healthcare-intake-extract-to-json.yaml +43 -0
- provekit/templates/flows/healthcare-intake-faq-match-answer.yaml +60 -0
- provekit/templates/flows/healthcare-intake-keyword-extract.yaml +43 -0
- provekit/templates/flows/healthcare-intake-language-route.yaml +103 -0
- provekit/templates/flows/healthcare-intake-moderation-gate.yaml +73 -0
- provekit/templates/flows/healthcare-intake-redact-summarize.yaml +59 -0
- provekit/templates/flows/healthcare-intake-score-gate.yaml +73 -0
- provekit/templates/flows/healthcare-intake-sentiment-route.yaml +73 -0
- provekit/templates/flows/healthcare-intake-severity-tiers.yaml +103 -0
- provekit/templates/flows/healthcare-intake-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/healthcare-intake-tag-enrich.yaml +43 -0
- provekit/templates/flows/healthcare-intake-tone-rewrite.yaml +43 -0
- provekit/templates/flows/healthcare-intake-translate-reply.yaml +58 -0
- provekit/templates/flows/healthcare-intake-triage-route.yaml +73 -0
- provekit/templates/flows/hr-action-items.yaml +43 -0
- provekit/templates/flows/hr-answer.yaml +43 -0
- provekit/templates/flows/hr-classify-intent.yaml +43 -0
- provekit/templates/flows/hr-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/hr-extract-to-json.yaml +43 -0
- provekit/templates/flows/hr-faq-match-answer.yaml +60 -0
- provekit/templates/flows/hr-keyword-extract.yaml +43 -0
- provekit/templates/flows/hr-language-route.yaml +103 -0
- provekit/templates/flows/hr-moderation-gate.yaml +73 -0
- provekit/templates/flows/hr-redact-summarize.yaml +59 -0
- provekit/templates/flows/hr-score-gate.yaml +73 -0
- provekit/templates/flows/hr-sentiment-route.yaml +73 -0
- provekit/templates/flows/hr-severity-tiers.yaml +103 -0
- provekit/templates/flows/hr-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/hr-tag-enrich.yaml +43 -0
- provekit/templates/flows/hr-tone-rewrite.yaml +43 -0
- provekit/templates/flows/hr-translate-reply.yaml +58 -0
- provekit/templates/flows/hr-triage-route.yaml +73 -0
- provekit/templates/flows/insurance-action-items.yaml +43 -0
- provekit/templates/flows/insurance-answer.yaml +43 -0
- provekit/templates/flows/insurance-classify-intent.yaml +43 -0
- provekit/templates/flows/insurance-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/insurance-extract-to-json.yaml +43 -0
- provekit/templates/flows/insurance-faq-match-answer.yaml +60 -0
- provekit/templates/flows/insurance-keyword-extract.yaml +43 -0
- provekit/templates/flows/insurance-language-route.yaml +103 -0
- provekit/templates/flows/insurance-moderation-gate.yaml +73 -0
- provekit/templates/flows/insurance-redact-summarize.yaml +59 -0
- provekit/templates/flows/insurance-score-gate.yaml +73 -0
- provekit/templates/flows/insurance-sentiment-route.yaml +73 -0
- provekit/templates/flows/insurance-severity-tiers.yaml +103 -0
- provekit/templates/flows/insurance-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/insurance-tag-enrich.yaml +43 -0
- provekit/templates/flows/insurance-tone-rewrite.yaml +43 -0
- provekit/templates/flows/insurance-translate-reply.yaml +58 -0
- provekit/templates/flows/insurance-triage-route.yaml +73 -0
- provekit/templates/flows/it-helpdesk-action-items.yaml +43 -0
- provekit/templates/flows/it-helpdesk-answer.yaml +43 -0
- provekit/templates/flows/it-helpdesk-classify-intent.yaml +43 -0
- provekit/templates/flows/it-helpdesk-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/it-helpdesk-extract-to-json.yaml +43 -0
- provekit/templates/flows/it-helpdesk-faq-match-answer.yaml +60 -0
- provekit/templates/flows/it-helpdesk-keyword-extract.yaml +43 -0
- provekit/templates/flows/it-helpdesk-language-route.yaml +103 -0
- provekit/templates/flows/it-helpdesk-moderation-gate.yaml +73 -0
- provekit/templates/flows/it-helpdesk-redact-summarize.yaml +59 -0
- provekit/templates/flows/it-helpdesk-score-gate.yaml +73 -0
- provekit/templates/flows/it-helpdesk-sentiment-route.yaml +73 -0
- provekit/templates/flows/it-helpdesk-severity-tiers.yaml +103 -0
- provekit/templates/flows/it-helpdesk-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/it-helpdesk-tag-enrich.yaml +43 -0
- provekit/templates/flows/it-helpdesk-tone-rewrite.yaml +43 -0
- provekit/templates/flows/it-helpdesk-translate-reply.yaml +58 -0
- provekit/templates/flows/it-helpdesk-triage-route.yaml +73 -0
- provekit/templates/flows/legal-intake-action-items.yaml +43 -0
- provekit/templates/flows/legal-intake-answer.yaml +43 -0
- provekit/templates/flows/legal-intake-classify-intent.yaml +43 -0
- provekit/templates/flows/legal-intake-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/legal-intake-extract-to-json.yaml +43 -0
- provekit/templates/flows/legal-intake-faq-match-answer.yaml +60 -0
- provekit/templates/flows/legal-intake-keyword-extract.yaml +43 -0
- provekit/templates/flows/legal-intake-language-route.yaml +103 -0
- provekit/templates/flows/legal-intake-moderation-gate.yaml +73 -0
- provekit/templates/flows/legal-intake-redact-summarize.yaml +59 -0
- provekit/templates/flows/legal-intake-score-gate.yaml +73 -0
- provekit/templates/flows/legal-intake-sentiment-route.yaml +73 -0
- provekit/templates/flows/legal-intake-severity-tiers.yaml +103 -0
- provekit/templates/flows/legal-intake-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/legal-intake-tag-enrich.yaml +43 -0
- provekit/templates/flows/legal-intake-tone-rewrite.yaml +43 -0
- provekit/templates/flows/legal-intake-translate-reply.yaml +58 -0
- provekit/templates/flows/legal-intake-triage-route.yaml +73 -0
- provekit/templates/flows/logistics-action-items.yaml +43 -0
- provekit/templates/flows/logistics-answer.yaml +43 -0
- provekit/templates/flows/logistics-classify-intent.yaml +43 -0
- provekit/templates/flows/logistics-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/logistics-extract-to-json.yaml +43 -0
- provekit/templates/flows/logistics-faq-match-answer.yaml +60 -0
- provekit/templates/flows/logistics-keyword-extract.yaml +43 -0
- provekit/templates/flows/logistics-language-route.yaml +103 -0
- provekit/templates/flows/logistics-moderation-gate.yaml +73 -0
- provekit/templates/flows/logistics-redact-summarize.yaml +59 -0
- provekit/templates/flows/logistics-score-gate.yaml +73 -0
- provekit/templates/flows/logistics-sentiment-route.yaml +73 -0
- provekit/templates/flows/logistics-severity-tiers.yaml +103 -0
- provekit/templates/flows/logistics-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/logistics-tag-enrich.yaml +43 -0
- provekit/templates/flows/logistics-tone-rewrite.yaml +43 -0
- provekit/templates/flows/logistics-translate-reply.yaml +58 -0
- provekit/templates/flows/logistics-triage-route.yaml +73 -0
- provekit/templates/flows/marketing-action-items.yaml +43 -0
- provekit/templates/flows/marketing-answer.yaml +43 -0
- provekit/templates/flows/marketing-classify-intent.yaml +43 -0
- provekit/templates/flows/marketing-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/marketing-extract-to-json.yaml +43 -0
- provekit/templates/flows/marketing-faq-match-answer.yaml +60 -0
- provekit/templates/flows/marketing-keyword-extract.yaml +43 -0
- provekit/templates/flows/marketing-language-route.yaml +103 -0
- provekit/templates/flows/marketing-moderation-gate.yaml +73 -0
- provekit/templates/flows/marketing-redact-summarize.yaml +59 -0
- provekit/templates/flows/marketing-score-gate.yaml +73 -0
- provekit/templates/flows/marketing-sentiment-route.yaml +73 -0
- provekit/templates/flows/marketing-severity-tiers.yaml +103 -0
- provekit/templates/flows/marketing-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/marketing-tag-enrich.yaml +43 -0
- provekit/templates/flows/marketing-tone-rewrite.yaml +43 -0
- provekit/templates/flows/marketing-translate-reply.yaml +58 -0
- provekit/templates/flows/marketing-triage-route.yaml +73 -0
- provekit/templates/flows/nonprofit-action-items.yaml +43 -0
- provekit/templates/flows/nonprofit-answer.yaml +43 -0
- provekit/templates/flows/nonprofit-classify-intent.yaml +43 -0
- provekit/templates/flows/nonprofit-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/nonprofit-extract-to-json.yaml +43 -0
- provekit/templates/flows/nonprofit-faq-match-answer.yaml +60 -0
- provekit/templates/flows/nonprofit-keyword-extract.yaml +43 -0
- provekit/templates/flows/nonprofit-language-route.yaml +103 -0
- provekit/templates/flows/nonprofit-moderation-gate.yaml +73 -0
- provekit/templates/flows/nonprofit-redact-summarize.yaml +59 -0
- provekit/templates/flows/nonprofit-score-gate.yaml +73 -0
- provekit/templates/flows/nonprofit-sentiment-route.yaml +73 -0
- provekit/templates/flows/nonprofit-severity-tiers.yaml +103 -0
- provekit/templates/flows/nonprofit-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/nonprofit-tag-enrich.yaml +43 -0
- provekit/templates/flows/nonprofit-tone-rewrite.yaml +43 -0
- provekit/templates/flows/nonprofit-translate-reply.yaml +58 -0
- provekit/templates/flows/nonprofit-triage-route.yaml +73 -0
- provekit/templates/flows/real-estate-action-items.yaml +43 -0
- provekit/templates/flows/real-estate-answer.yaml +43 -0
- provekit/templates/flows/real-estate-classify-intent.yaml +43 -0
- provekit/templates/flows/real-estate-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/real-estate-extract-to-json.yaml +43 -0
- provekit/templates/flows/real-estate-faq-match-answer.yaml +60 -0
- provekit/templates/flows/real-estate-keyword-extract.yaml +43 -0
- provekit/templates/flows/real-estate-language-route.yaml +103 -0
- provekit/templates/flows/real-estate-moderation-gate.yaml +73 -0
- provekit/templates/flows/real-estate-redact-summarize.yaml +59 -0
- provekit/templates/flows/real-estate-score-gate.yaml +73 -0
- provekit/templates/flows/real-estate-sentiment-route.yaml +73 -0
- provekit/templates/flows/real-estate-severity-tiers.yaml +103 -0
- provekit/templates/flows/real-estate-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/real-estate-tag-enrich.yaml +43 -0
- provekit/templates/flows/real-estate-tone-rewrite.yaml +43 -0
- provekit/templates/flows/real-estate-translate-reply.yaml +58 -0
- provekit/templates/flows/real-estate-triage-route.yaml +73 -0
- provekit/templates/flows/recruiting-action-items.yaml +43 -0
- provekit/templates/flows/recruiting-answer.yaml +43 -0
- provekit/templates/flows/recruiting-classify-intent.yaml +43 -0
- provekit/templates/flows/recruiting-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/recruiting-extract-to-json.yaml +43 -0
- provekit/templates/flows/recruiting-faq-match-answer.yaml +60 -0
- provekit/templates/flows/recruiting-keyword-extract.yaml +43 -0
- provekit/templates/flows/recruiting-language-route.yaml +103 -0
- provekit/templates/flows/recruiting-moderation-gate.yaml +73 -0
- provekit/templates/flows/recruiting-redact-summarize.yaml +59 -0
- provekit/templates/flows/recruiting-score-gate.yaml +73 -0
- provekit/templates/flows/recruiting-sentiment-route.yaml +73 -0
- provekit/templates/flows/recruiting-severity-tiers.yaml +103 -0
- provekit/templates/flows/recruiting-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/recruiting-tag-enrich.yaml +43 -0
- provekit/templates/flows/recruiting-tone-rewrite.yaml +43 -0
- provekit/templates/flows/recruiting-translate-reply.yaml +58 -0
- provekit/templates/flows/recruiting-triage-route.yaml +73 -0
- provekit/templates/flows/research-action-items.yaml +43 -0
- provekit/templates/flows/research-answer.yaml +43 -0
- provekit/templates/flows/research-classify-intent.yaml +43 -0
- provekit/templates/flows/research-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/research-extract-to-json.yaml +43 -0
- provekit/templates/flows/research-faq-match-answer.yaml +60 -0
- provekit/templates/flows/research-keyword-extract.yaml +43 -0
- provekit/templates/flows/research-language-route.yaml +103 -0
- provekit/templates/flows/research-moderation-gate.yaml +73 -0
- provekit/templates/flows/research-redact-summarize.yaml +59 -0
- provekit/templates/flows/research-score-gate.yaml +73 -0
- provekit/templates/flows/research-sentiment-route.yaml +73 -0
- provekit/templates/flows/research-severity-tiers.yaml +103 -0
- provekit/templates/flows/research-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/research-tag-enrich.yaml +43 -0
- provekit/templates/flows/research-tone-rewrite.yaml +43 -0
- provekit/templates/flows/research-translate-reply.yaml +58 -0
- provekit/templates/flows/research-triage-route.yaml +73 -0
- provekit/templates/flows/sales-lead-action-items.yaml +43 -0
- provekit/templates/flows/sales-lead-answer.yaml +43 -0
- provekit/templates/flows/sales-lead-classify-intent.yaml +43 -0
- provekit/templates/flows/sales-lead-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/sales-lead-extract-to-json.yaml +43 -0
- provekit/templates/flows/sales-lead-faq-match-answer.yaml +60 -0
- provekit/templates/flows/sales-lead-keyword-extract.yaml +43 -0
- provekit/templates/flows/sales-lead-language-route.yaml +103 -0
- provekit/templates/flows/sales-lead-moderation-gate.yaml +73 -0
- provekit/templates/flows/sales-lead-redact-summarize.yaml +59 -0
- provekit/templates/flows/sales-lead-score-gate.yaml +73 -0
- provekit/templates/flows/sales-lead-sentiment-route.yaml +73 -0
- provekit/templates/flows/sales-lead-severity-tiers.yaml +103 -0
- provekit/templates/flows/sales-lead-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/sales-lead-tag-enrich.yaml +43 -0
- provekit/templates/flows/sales-lead-tone-rewrite.yaml +43 -0
- provekit/templates/flows/sales-lead-translate-reply.yaml +58 -0
- provekit/templates/flows/sales-lead-triage-route.yaml +73 -0
- provekit/templates/flows/travel-action-items.yaml +43 -0
- provekit/templates/flows/travel-answer.yaml +43 -0
- provekit/templates/flows/travel-classify-intent.yaml +43 -0
- provekit/templates/flows/travel-draft-critique-revise.yaml +75 -0
- provekit/templates/flows/travel-extract-to-json.yaml +43 -0
- provekit/templates/flows/travel-faq-match-answer.yaml +60 -0
- provekit/templates/flows/travel-keyword-extract.yaml +43 -0
- provekit/templates/flows/travel-language-route.yaml +103 -0
- provekit/templates/flows/travel-moderation-gate.yaml +73 -0
- provekit/templates/flows/travel-redact-summarize.yaml +59 -0
- provekit/templates/flows/travel-score-gate.yaml +73 -0
- provekit/templates/flows/travel-sentiment-route.yaml +73 -0
- provekit/templates/flows/travel-severity-tiers.yaml +103 -0
- provekit/templates/flows/travel-summarize-then-decide.yaml +88 -0
- provekit/templates/flows/travel-tag-enrich.yaml +43 -0
- provekit/templates/flows/travel-tone-rewrite.yaml +43 -0
- provekit/templates/flows/travel-translate-reply.yaml +58 -0
- provekit/templates/flows/travel-triage-route.yaml +73 -0
- provekit-0.1.0.dist-info/METADATA +86 -0
- provekit-0.1.0.dist-info/RECORD +485 -0
- provekit-0.1.0.dist-info/WHEEL +5 -0
- provekit-0.1.0.dist-info/entry_points.txt +2 -0
- provekit-0.1.0.dist-info/top_level.txt +1 -0
provekit/routers/run.py
ADDED
|
@@ -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
|