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
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Generic MCP client — the best test client for Model Context Protocol servers.
|
|
2
|
+
|
|
3
|
+
Transports: Streamable HTTP · stdio (spawn a local server process)
|
|
4
|
+
Spec generations: stateful 2025-11-25 (initialize handshake + Mcp-Session-Id)
|
|
5
|
+
stateless 2026-07-28 (no session, server/discover) · auto-detect
|
|
6
|
+
Auth: static bearer (via headers) · OAuth 2.1 client-credentials grant;
|
|
7
|
+
a 401 surfaces the resource-metadata hint so the user knows what's needed.
|
|
8
|
+
Discovery: tools · resources · prompts, all cursor-paginated.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import contextlib
|
|
13
|
+
import json
|
|
14
|
+
import subprocess
|
|
15
|
+
import threading
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
from ..netguard import guard_stdio, guard_url
|
|
20
|
+
|
|
21
|
+
_ACCEPT = "application/json, text/event-stream"
|
|
22
|
+
_STATEFUL = "2025-11-25"
|
|
23
|
+
_STATELESS = "2026-07-28"
|
|
24
|
+
_lock = threading.Lock()
|
|
25
|
+
_id = 0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _next_id() -> int:
|
|
29
|
+
global _id
|
|
30
|
+
with _lock:
|
|
31
|
+
_id += 1
|
|
32
|
+
return _id
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MCPError(RuntimeError):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _fetch_oauth_token(oauth: dict) -> str:
|
|
40
|
+
"""Exchange client credentials for a bearer token (OAuth 2.1 client_credentials).
|
|
41
|
+
Interactive auth-code/PKCE flows need a browser and are out of scope here."""
|
|
42
|
+
token_url = oauth.get("token_url")
|
|
43
|
+
if not token_url:
|
|
44
|
+
raise MCPError("oauth config needs a token_url")
|
|
45
|
+
guard_url(token_url)
|
|
46
|
+
data = {"grant_type": "client_credentials",
|
|
47
|
+
"client_id": oauth.get("client_id", ""),
|
|
48
|
+
"client_secret": oauth.get("client_secret", "")}
|
|
49
|
+
if oauth.get("scope"):
|
|
50
|
+
data["scope"] = oauth["scope"]
|
|
51
|
+
if oauth.get("resource"): # RFC 8707 resource indicator
|
|
52
|
+
data["resource"] = oauth["resource"]
|
|
53
|
+
r = httpx.post(token_url, data=data, timeout=30, follow_redirects=False)
|
|
54
|
+
r.raise_for_status()
|
|
55
|
+
tok = r.json().get("access_token")
|
|
56
|
+
if not tok:
|
|
57
|
+
raise MCPError("no access_token in the OAuth token response")
|
|
58
|
+
return tok
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _HTTPTransport:
|
|
62
|
+
def __init__(self, url, headers, timeout, stateful: bool):
|
|
63
|
+
guard_url(url)
|
|
64
|
+
self.url = url.rstrip("/")
|
|
65
|
+
self.headers = {"Content-Type": "application/json", "Accept": _ACCEPT, **(headers or {})}
|
|
66
|
+
self.stateful = stateful
|
|
67
|
+
self.session_id: str | None = None
|
|
68
|
+
self._client = httpx.Client(timeout=timeout, follow_redirects=False)
|
|
69
|
+
|
|
70
|
+
def _hdrs(self):
|
|
71
|
+
h = dict(self.headers)
|
|
72
|
+
if self.session_id:
|
|
73
|
+
h["Mcp-Session-Id"] = self.session_id
|
|
74
|
+
return h
|
|
75
|
+
|
|
76
|
+
def request(self, payload) -> dict:
|
|
77
|
+
resp = self._client.post(self.url, json=payload, headers=self._hdrs())
|
|
78
|
+
sid = resp.headers.get("mcp-session-id")
|
|
79
|
+
if sid:
|
|
80
|
+
self.session_id = sid
|
|
81
|
+
if resp.status_code == 401:
|
|
82
|
+
meta = resp.headers.get("www-authenticate", "")
|
|
83
|
+
raise MCPError(f"401 Unauthorized — server requires OAuth. {meta}".strip())
|
|
84
|
+
resp.raise_for_status()
|
|
85
|
+
return _parse_http(resp)
|
|
86
|
+
|
|
87
|
+
def notify(self, payload) -> None:
|
|
88
|
+
self._client.post(self.url, json=payload, headers=self._hdrs())
|
|
89
|
+
|
|
90
|
+
def close(self):
|
|
91
|
+
self._client.close()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class _StdioTransport:
|
|
95
|
+
"""Newline-delimited JSON-RPC over a spawned process's stdin/stdout."""
|
|
96
|
+
def __init__(self, command, args, env, timeout):
|
|
97
|
+
guard_stdio() # RCE gate: never spawn a local process in hosted mode
|
|
98
|
+
self.proc = subprocess.Popen(
|
|
99
|
+
[command, *(args or [])], stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
100
|
+
stderr=subprocess.DEVNULL, text=True, bufsize=1, env=env)
|
|
101
|
+
|
|
102
|
+
def request(self, payload) -> dict:
|
|
103
|
+
assert self.proc.stdin and self.proc.stdout
|
|
104
|
+
self.proc.stdin.write(json.dumps(payload) + "\n")
|
|
105
|
+
self.proc.stdin.flush()
|
|
106
|
+
want = payload.get("id")
|
|
107
|
+
for line in self.proc.stdout:
|
|
108
|
+
line = line.strip()
|
|
109
|
+
if not line:
|
|
110
|
+
continue
|
|
111
|
+
msg = json.loads(line)
|
|
112
|
+
if msg.get("id") == want:
|
|
113
|
+
return msg
|
|
114
|
+
raise MCPError("stdio server closed before replying")
|
|
115
|
+
|
|
116
|
+
def notify(self, payload) -> None:
|
|
117
|
+
assert self.proc.stdin
|
|
118
|
+
self.proc.stdin.write(json.dumps(payload) + "\n")
|
|
119
|
+
self.proc.stdin.flush()
|
|
120
|
+
|
|
121
|
+
def close(self):
|
|
122
|
+
try:
|
|
123
|
+
self.proc.terminate()
|
|
124
|
+
self.proc.wait(timeout=5)
|
|
125
|
+
except Exception:
|
|
126
|
+
self.proc.kill()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _parse_http(resp: httpx.Response) -> dict:
|
|
130
|
+
ctype = resp.headers.get("content-type", "")
|
|
131
|
+
if ctype.startswith("text/event-stream"):
|
|
132
|
+
for line in resp.text.splitlines():
|
|
133
|
+
if line.startswith("data:"):
|
|
134
|
+
chunk = json.loads(line[5:].strip())
|
|
135
|
+
if isinstance(chunk, dict) and ("result" in chunk or "error" in chunk):
|
|
136
|
+
return chunk
|
|
137
|
+
raise MCPError("no JSON-RPC message in SSE stream")
|
|
138
|
+
return resp.json()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class MCPSession:
|
|
142
|
+
"""A short-lived MCP session over one transport.
|
|
143
|
+
|
|
144
|
+
HTTP: MCPSession(url, headers=..., spec="auto"|"2025-11-25"|"2026-07-28", oauth=...)
|
|
145
|
+
stdio: MCPSession(command="python", args=["server.py"], env=...)
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
# Class-level defaults so a session built via __new__ (tests inject a fake transport)
|
|
149
|
+
# still has them.
|
|
150
|
+
_inited = False
|
|
151
|
+
_keep_open = False
|
|
152
|
+
|
|
153
|
+
def __init__(self, url: str | None = None, headers: dict | None = None, timeout: float = 30, *,
|
|
154
|
+
command: str | None = None, args: list | None = None, env: dict | None = None,
|
|
155
|
+
spec: str = "auto", oauth: dict | None = None):
|
|
156
|
+
headers = dict(headers or {})
|
|
157
|
+
if oauth:
|
|
158
|
+
headers.setdefault("Authorization", f"Bearer {_fetch_oauth_token(oauth)}")
|
|
159
|
+
self.spec = spec
|
|
160
|
+
self._inited = False
|
|
161
|
+
self._keep_open = False
|
|
162
|
+
self._stdio = command is not None
|
|
163
|
+
if self._stdio:
|
|
164
|
+
self._t = _StdioTransport(command, args, env, timeout)
|
|
165
|
+
self._stateful = True # stdio is always a stateful session
|
|
166
|
+
else:
|
|
167
|
+
if not url:
|
|
168
|
+
raise MCPError("MCP session needs a url (HTTP) or command (stdio)")
|
|
169
|
+
self._stateful = spec != _STATELESS
|
|
170
|
+
self._t = _HTTPTransport(url, headers, timeout, self._stateful)
|
|
171
|
+
|
|
172
|
+
def _rpc(self, method: str, params: dict | None = None) -> dict:
|
|
173
|
+
payload = {"jsonrpc": "2.0", "id": _next_id(), "method": method, "params": params or {}}
|
|
174
|
+
data = self._t.request(payload)
|
|
175
|
+
if "error" in data:
|
|
176
|
+
raise MCPError(str(data["error"].get("message", "MCP error")))
|
|
177
|
+
return data.get("result", {})
|
|
178
|
+
|
|
179
|
+
def open(self) -> "MCPSession":
|
|
180
|
+
"""Keep the transport open across calls until close(). list_tools()/call_tool()
|
|
181
|
+
otherwise close after each call, so a tool-calling loop would re-handshake every
|
|
182
|
+
time — and for stdio, spawn and kill a subprocess every time."""
|
|
183
|
+
self._keep_open = True
|
|
184
|
+
return self
|
|
185
|
+
|
|
186
|
+
def close(self) -> None:
|
|
187
|
+
self._keep_open = False
|
|
188
|
+
self._close()
|
|
189
|
+
|
|
190
|
+
@contextlib.contextmanager
|
|
191
|
+
def session(self):
|
|
192
|
+
"""open() for the duration of a block. The session is spent afterwards, exactly as
|
|
193
|
+
it is after a bare call today."""
|
|
194
|
+
try:
|
|
195
|
+
yield self.open()
|
|
196
|
+
finally:
|
|
197
|
+
self.close()
|
|
198
|
+
|
|
199
|
+
def _close(self) -> None:
|
|
200
|
+
self._inited = False
|
|
201
|
+
self._t.close()
|
|
202
|
+
|
|
203
|
+
def _close_unless_kept(self) -> None:
|
|
204
|
+
if not self._keep_open:
|
|
205
|
+
self._close()
|
|
206
|
+
|
|
207
|
+
def _init(self) -> None:
|
|
208
|
+
if self._inited or not self._stateful:
|
|
209
|
+
return # stateless generation: no handshake, no session id
|
|
210
|
+
try:
|
|
211
|
+
self._rpc("initialize", {
|
|
212
|
+
"protocolVersion": _STATEFUL if self.spec == "auto" else self.spec,
|
|
213
|
+
"capabilities": {},
|
|
214
|
+
"clientInfo": {"name": "provekit", "version": "0.1.0"},
|
|
215
|
+
})
|
|
216
|
+
self._t.notify({"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}})
|
|
217
|
+
self._inited = True
|
|
218
|
+
except Exception:
|
|
219
|
+
# auto: a server that rejects initialize is treated as stateless.
|
|
220
|
+
if self.spec == "auto":
|
|
221
|
+
self._stateful = False
|
|
222
|
+
|
|
223
|
+
def _paginate(self, method: str, key: str) -> list[dict]:
|
|
224
|
+
items, cursor = [], None
|
|
225
|
+
while True:
|
|
226
|
+
res = self._rpc(method, {"cursor": cursor} if cursor else {})
|
|
227
|
+
items += res.get(key, [])
|
|
228
|
+
cursor = res.get("nextCursor")
|
|
229
|
+
if not cursor:
|
|
230
|
+
return items
|
|
231
|
+
|
|
232
|
+
def list_tools(self) -> list[dict]:
|
|
233
|
+
try:
|
|
234
|
+
self._init()
|
|
235
|
+
tools = self._paginate("tools/list", "tools")
|
|
236
|
+
return [{"name": t["name"], "description": t.get("description", ""),
|
|
237
|
+
"input_schema": t.get("inputSchema") or {}} for t in tools]
|
|
238
|
+
finally:
|
|
239
|
+
self._close_unless_kept()
|
|
240
|
+
|
|
241
|
+
def list_resources(self) -> list[dict]:
|
|
242
|
+
try:
|
|
243
|
+
self._init()
|
|
244
|
+
res = self._paginate("resources/list", "resources")
|
|
245
|
+
return [{"uri": r.get("uri"), "name": r.get("name", ""),
|
|
246
|
+
"mime_type": r.get("mimeType", "")} for r in res]
|
|
247
|
+
finally:
|
|
248
|
+
self._close_unless_kept()
|
|
249
|
+
|
|
250
|
+
def list_prompts(self) -> list[dict]:
|
|
251
|
+
try:
|
|
252
|
+
self._init()
|
|
253
|
+
pr = self._paginate("prompts/list", "prompts")
|
|
254
|
+
return [{"name": p["name"], "description": p.get("description", ""),
|
|
255
|
+
"arguments": p.get("arguments", [])} for p in pr]
|
|
256
|
+
finally:
|
|
257
|
+
self._close_unless_kept()
|
|
258
|
+
|
|
259
|
+
def call_tool(self, name: str, args: dict) -> dict:
|
|
260
|
+
try:
|
|
261
|
+
self._init()
|
|
262
|
+
res = self._rpc("tools/call", {"name": name, "arguments": args or {}})
|
|
263
|
+
finally:
|
|
264
|
+
self._close_unless_kept()
|
|
265
|
+
if res.get("isError"):
|
|
266
|
+
raise MCPError(_text(res) or f"tool '{name}' failed")
|
|
267
|
+
return _unwrap(res)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _text(res: dict) -> str | None:
|
|
271
|
+
for block in res.get("content", []):
|
|
272
|
+
if block.get("type") == "text":
|
|
273
|
+
return block.get("text")
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _unwrap(res: dict):
|
|
278
|
+
sc = res.get("structuredContent")
|
|
279
|
+
if sc is not None:
|
|
280
|
+
if isinstance(sc, dict) and set(sc.keys()) == {"result"}:
|
|
281
|
+
return sc["result"]
|
|
282
|
+
return sc
|
|
283
|
+
txt = _text(res)
|
|
284
|
+
if txt is None:
|
|
285
|
+
return res.get("content", res)
|
|
286
|
+
try:
|
|
287
|
+
return json.loads(txt)
|
|
288
|
+
except json.JSONDecodeError:
|
|
289
|
+
return {"text": txt}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Storage for paused flow-run contexts (breakpoint/step debugging).
|
|
2
|
+
|
|
3
|
+
In-memory by default (single-process local use). When REDIS_URL is set, contexts live in
|
|
4
|
+
Redis so step-debugging survives across uvicorn workers and restarts — pop uses GETDEL so
|
|
5
|
+
two concurrent /continue calls can't resume the same run twice.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
|
|
13
|
+
from ..config import get_settings
|
|
14
|
+
|
|
15
|
+
_TTL = 1800 # evict paused runs abandoned for 30 min
|
|
16
|
+
_MAX = 200 # in-memory hard cap
|
|
17
|
+
_MAX_BYTES = 1_000_000 # reject a ctx that serializes larger than ~1MB
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _MemoryStore:
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self._d: dict[str, dict] = {}
|
|
23
|
+
self._ts: dict[str, float] = {}
|
|
24
|
+
|
|
25
|
+
def store(self, rid: str, ctx: dict) -> None:
|
|
26
|
+
now = time.monotonic()
|
|
27
|
+
self._d[rid] = ctx
|
|
28
|
+
self._ts[rid] = now
|
|
29
|
+
for k in [k for k, t in list(self._ts.items()) if now - t > _TTL]:
|
|
30
|
+
self._d.pop(k, None); self._ts.pop(k, None)
|
|
31
|
+
if len(self._d) > _MAX:
|
|
32
|
+
for k in sorted(self._ts, key=self._ts.get)[: len(self._d) - _MAX]:
|
|
33
|
+
self._d.pop(k, None); self._ts.pop(k, None)
|
|
34
|
+
|
|
35
|
+
def get(self, rid: str) -> dict | None:
|
|
36
|
+
return self._d.get(rid)
|
|
37
|
+
|
|
38
|
+
def pop(self, rid: str) -> dict | None:
|
|
39
|
+
self._ts.pop(rid, None)
|
|
40
|
+
return self._d.pop(rid, None)
|
|
41
|
+
|
|
42
|
+
def drop(self, rid: str) -> None:
|
|
43
|
+
self._d.pop(rid, None); self._ts.pop(rid, None)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class _RedisStore:
|
|
47
|
+
_PREFIX = "agm:run:"
|
|
48
|
+
|
|
49
|
+
def __init__(self, url: str):
|
|
50
|
+
import redis # imported only when REDIS_URL is set
|
|
51
|
+
self._r = redis.Redis.from_url(url, decode_responses=True)
|
|
52
|
+
|
|
53
|
+
def _k(self, rid: str) -> str:
|
|
54
|
+
return self._PREFIX + rid
|
|
55
|
+
|
|
56
|
+
def store(self, rid: str, ctx: dict) -> None:
|
|
57
|
+
blob = json.dumps(ctx)
|
|
58
|
+
if len(blob) > _MAX_BYTES:
|
|
59
|
+
raise ValueError("run context too large to persist")
|
|
60
|
+
self._r.setex(self._k(rid), _TTL, blob)
|
|
61
|
+
|
|
62
|
+
def get(self, rid: str) -> dict | None:
|
|
63
|
+
blob = self._r.get(self._k(rid))
|
|
64
|
+
return json.loads(blob) if blob else None
|
|
65
|
+
|
|
66
|
+
def pop(self, rid: str) -> dict | None:
|
|
67
|
+
blob = self._r.getdel(self._k(rid)) # atomic — prevents double-resume
|
|
68
|
+
return json.loads(blob) if blob else None
|
|
69
|
+
|
|
70
|
+
def drop(self, rid: str) -> None:
|
|
71
|
+
self._r.delete(self._k(rid))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@lru_cache
|
|
75
|
+
def _store():
|
|
76
|
+
url = get_settings().redis_url
|
|
77
|
+
return _RedisStore(url) if url else _MemoryStore()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def store_ctx(rid: str, ctx: dict) -> None:
|
|
81
|
+
_store().store(rid, ctx)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_ctx(rid: str) -> dict | None:
|
|
85
|
+
return _store().get(rid)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def pop_ctx(rid: str) -> dict | None:
|
|
89
|
+
return _store().pop(rid)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def drop_ctx(rid: str) -> None:
|
|
93
|
+
_store().drop(rid)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Encrypt connection secrets at rest (Fernet). Sealed values carry an "enc:" prefix;
|
|
2
|
+
plaintext values from older databases are accepted on read and upgraded by the
|
|
3
|
+
one-shot reseal pass at startup (and on any subsequent write).
|
|
4
|
+
|
|
5
|
+
Key source: SECRET_KEY env var (any string — derived to a Fernet key), or, for local
|
|
6
|
+
SQLite use, an auto-generated key file stored next to the database (.provekit.key).
|
|
7
|
+
Rotation: set the new SECRET_KEY, then re-enter credentials (values sealed with the
|
|
8
|
+
old key decrypt to "" with a logged warning rather than crashing).
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import base64
|
|
13
|
+
import hashlib
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
from functools import lru_cache
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from cryptography.fernet import Fernet, InvalidToken
|
|
20
|
+
|
|
21
|
+
from ..config import get_settings
|
|
22
|
+
from .masking import is_secret_header
|
|
23
|
+
|
|
24
|
+
_PREFIX = "enc:"
|
|
25
|
+
log = logging.getLogger("provekit.sealing")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _key_file() -> Path | None:
|
|
29
|
+
url = get_settings().database_url
|
|
30
|
+
if url.startswith("sqlite:///"):
|
|
31
|
+
db_path = Path(url.removeprefix("sqlite:///"))
|
|
32
|
+
parent = db_path.parent if str(db_path.parent) else Path(".")
|
|
33
|
+
return parent / ".provekit.key"
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@lru_cache
|
|
38
|
+
def _fernet() -> Fernet:
|
|
39
|
+
s = get_settings()
|
|
40
|
+
if s.secret_key:
|
|
41
|
+
return Fernet(base64.urlsafe_b64encode(hashlib.sha256(s.secret_key.encode()).digest()))
|
|
42
|
+
kf = _key_file()
|
|
43
|
+
if kf is None:
|
|
44
|
+
raise RuntimeError("SECRET_KEY must be set when not using a local SQLite database")
|
|
45
|
+
if kf.exists():
|
|
46
|
+
return Fernet(kf.read_bytes().strip())
|
|
47
|
+
key = Fernet.generate_key()
|
|
48
|
+
kf.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
kf.write_bytes(key)
|
|
50
|
+
try:
|
|
51
|
+
os.chmod(kf, 0o600)
|
|
52
|
+
except OSError:
|
|
53
|
+
pass
|
|
54
|
+
log.info("generated local encryption key at %s", kf)
|
|
55
|
+
return Fernet(key)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def seal(value: str) -> str:
|
|
59
|
+
if not value or not isinstance(value, str) or value.startswith(_PREFIX):
|
|
60
|
+
return value
|
|
61
|
+
return _PREFIX + _fernet().encrypt(value.encode()).decode()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def unseal(value):
|
|
65
|
+
if not isinstance(value, str) or not value.startswith(_PREFIX):
|
|
66
|
+
return value
|
|
67
|
+
try:
|
|
68
|
+
return _fernet().decrypt(value[len(_PREFIX):].encode()).decode()
|
|
69
|
+
except (InvalidToken, ValueError):
|
|
70
|
+
log.warning("could not decrypt a stored secret (SECRET_KEY changed?) — treating as unset")
|
|
71
|
+
return ""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def seal_config(cfg: dict) -> dict:
|
|
75
|
+
"""Encrypt the secret fields of a connection config: api_key, secret headers,
|
|
76
|
+
the MCP OAuth client_secret, and stdio env values (which routinely carry tokens)."""
|
|
77
|
+
out = dict(cfg)
|
|
78
|
+
if out.get("api_key"):
|
|
79
|
+
out["api_key"] = seal(out["api_key"])
|
|
80
|
+
hdrs = out.get("headers")
|
|
81
|
+
if isinstance(hdrs, dict):
|
|
82
|
+
out["headers"] = {k: (seal(v) if is_secret_header(k) and isinstance(v, str) and v else v)
|
|
83
|
+
for k, v in hdrs.items()}
|
|
84
|
+
oauth = out.get("oauth")
|
|
85
|
+
if isinstance(oauth, dict) and oauth.get("client_secret"):
|
|
86
|
+
out["oauth"] = {**oauth, "client_secret": seal(oauth["client_secret"])}
|
|
87
|
+
env = out.get("env")
|
|
88
|
+
if isinstance(env, dict):
|
|
89
|
+
out["env"] = {k: (seal(v) if isinstance(v, str) and v else v) for k, v in env.items()}
|
|
90
|
+
return out
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def unseal_config(cfg: dict) -> dict:
|
|
94
|
+
out = dict(cfg)
|
|
95
|
+
if out.get("api_key"):
|
|
96
|
+
out["api_key"] = unseal(out["api_key"])
|
|
97
|
+
hdrs = out.get("headers")
|
|
98
|
+
if isinstance(hdrs, dict):
|
|
99
|
+
out["headers"] = {k: unseal(v) for k, v in hdrs.items()}
|
|
100
|
+
oauth = out.get("oauth")
|
|
101
|
+
if isinstance(oauth, dict) and "client_secret" in oauth:
|
|
102
|
+
out["oauth"] = {**oauth, "client_secret": unseal(oauth.get("client_secret"))}
|
|
103
|
+
env = out.get("env")
|
|
104
|
+
if isinstance(env, dict):
|
|
105
|
+
out["env"] = {k: unseal(v) for k, v in env.items()}
|
|
106
|
+
return out
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Bundled flow templates — the searchable gallery served to the New-flow picker."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import pathlib
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
|
|
8
|
+
from . import testfile
|
|
9
|
+
|
|
10
|
+
_DIR = pathlib.Path(__file__).resolve().parent.parent / "templates" / "flows"
|
|
11
|
+
|
|
12
|
+
# A small, hand-picked showcase surfaced at the top of the New-flow picker. The gallery is a
|
|
13
|
+
# regular 24-domain × 18-pattern grid (432 entries), which reads as a wall of near-duplicates;
|
|
14
|
+
# these are eight *distinct patterns* across eight *distinct domains* — each a different
|
|
15
|
+
# capability (branching, self-critique, structured output, safety gate, numeric gate,
|
|
16
|
+
# language routing) — so a newcomer sees a few great starting points instead of everything.
|
|
17
|
+
_FEATURED = [
|
|
18
|
+
"customer-support-triage-route",
|
|
19
|
+
"sales-lead-draft-critique-revise",
|
|
20
|
+
"healthcare-intake-extract-to-json",
|
|
21
|
+
"content-moderation-moderation-gate",
|
|
22
|
+
"legal-intake-summarize-then-decide",
|
|
23
|
+
"e-commerce-sentiment-route",
|
|
24
|
+
"recruiting-score-gate",
|
|
25
|
+
"travel-language-route",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@lru_cache
|
|
30
|
+
def _manifest() -> list[dict]:
|
|
31
|
+
f = _DIR / "_manifest.json"
|
|
32
|
+
return json.loads(f.read_text()) if f.exists() else []
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def featured() -> list[dict]:
|
|
36
|
+
"""The curated showcase, in order. Skips any slug missing from the manifest so a
|
|
37
|
+
renamed template can't break the picker."""
|
|
38
|
+
by_slug = {m["slug"]: m for m in _manifest()}
|
|
39
|
+
return [by_slug[s] for s in _FEATURED if s in by_slug]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def search(q: str = "", limit: int = 60) -> list[dict]:
|
|
43
|
+
"""Filter templates by a case-insensitive substring over name/description/category."""
|
|
44
|
+
items = _manifest()
|
|
45
|
+
q = (q or "").strip().lower()
|
|
46
|
+
if q:
|
|
47
|
+
items = [m for m in items
|
|
48
|
+
if q in m["name"].lower() or q in m["description"].lower() or q in m["category"].lower()]
|
|
49
|
+
return items[:limit]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def categories() -> list[str]:
|
|
53
|
+
return sorted({m["category"] for m in _manifest()})
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def total() -> int:
|
|
57
|
+
return len(_manifest())
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def load(slug: str) -> dict | None:
|
|
61
|
+
"""Return the parsed .provekit flow doc for a template slug (None if unknown)."""
|
|
62
|
+
# Reject separators/traversal (incl. Windows drive-relative "C:foo"), then confirm the
|
|
63
|
+
# resolved path stays inside the templates dir before reading.
|
|
64
|
+
if not slug or any(ch in slug for ch in ("/", "\\", ":")) or ".." in slug:
|
|
65
|
+
return None
|
|
66
|
+
f = (_DIR / f"{slug}.yaml").resolve()
|
|
67
|
+
if _DIR not in f.parents or not f.exists():
|
|
68
|
+
return None
|
|
69
|
+
return testfile.load(f.read_text())
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""The .provekit file format — plain-text, git-diffable tests and flows.
|
|
2
|
+
|
|
3
|
+
A *test* file bundles one request (prompt | tool | agent), its assertions, and an
|
|
4
|
+
optional dataset of input rows — enough to re-run it anywhere, including CI.
|
|
5
|
+
A *flow* file carries a visual flow's graph. Connections are referenced BY NAME:
|
|
6
|
+
credentials never enter the file; they resolve against the local workspace at
|
|
7
|
+
import/run time.
|
|
8
|
+
|
|
9
|
+
Canonical test shape (YAML, stable field order):
|
|
10
|
+
|
|
11
|
+
version: 1
|
|
12
|
+
kind: test
|
|
13
|
+
name: Support bot regression
|
|
14
|
+
connection: OpenAI (prod)
|
|
15
|
+
request:
|
|
16
|
+
type: prompt
|
|
17
|
+
model: gpt-4o-mini
|
|
18
|
+
system: You are a support assistant.
|
|
19
|
+
user: "{{message}}"
|
|
20
|
+
assertions:
|
|
21
|
+
- type: contains
|
|
22
|
+
value: refund
|
|
23
|
+
dataset:
|
|
24
|
+
- name: angry customer
|
|
25
|
+
variables: {message: "URGENT: refund now"}
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import yaml
|
|
30
|
+
|
|
31
|
+
VERSION = 1
|
|
32
|
+
KINDS = ("test", "flow")
|
|
33
|
+
|
|
34
|
+
# Stable, human-friendly field order per request type.
|
|
35
|
+
_REQ_ORDER = {
|
|
36
|
+
"prompt": ["type", "model", "system", "user", "messages", "temperature", "max_tokens"],
|
|
37
|
+
"tool": ["type", "tool", "args"],
|
|
38
|
+
"agent": ["type", "method", "path", "headers", "body", "stream"],
|
|
39
|
+
}
|
|
40
|
+
_ASSERT_ORDER = ["type", "name", "path", "value", "schema", "criteria", "model"]
|
|
41
|
+
|
|
42
|
+
# Never serialized: secrets and workspace-local ids.
|
|
43
|
+
_STRIP = {"api_key", "connection_id", "assertions", "_k", "provider", "base_url", "url"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _ordered(d: dict, order: list[str]) -> dict:
|
|
47
|
+
out = {k: d[k] for k in order if k in d and d[k] not in (None, "", {}, [])}
|
|
48
|
+
out.update({k: v for k, v in d.items() if k not in out and k not in order})
|
|
49
|
+
return out
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _clean_request(request: dict) -> dict:
|
|
53
|
+
req = {k: v for k, v in request.items() if k not in _STRIP}
|
|
54
|
+
return _ordered(req, _REQ_ORDER.get(request.get("type", ""), ["type"]))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def dump_test(name: str, request: dict, connection_name: str | None = None,
|
|
58
|
+
dataset: list[dict] | None = None) -> str:
|
|
59
|
+
doc: dict = {"version": VERSION, "kind": "test", "name": name or "test"}
|
|
60
|
+
if connection_name:
|
|
61
|
+
doc["connection"] = connection_name
|
|
62
|
+
doc["request"] = _clean_request(request)
|
|
63
|
+
asserts = request.get("assertions") or []
|
|
64
|
+
if asserts:
|
|
65
|
+
doc["assertions"] = [_ordered(dict(a), _ASSERT_ORDER) for a in asserts]
|
|
66
|
+
if dataset:
|
|
67
|
+
doc["dataset"] = [{"name": r.get("name") or f"row {i + 1}", "variables": r.get("variables") or {}}
|
|
68
|
+
for i, r in enumerate(dataset)]
|
|
69
|
+
return yaml.safe_dump(doc, sort_keys=False, allow_unicode=True, width=100)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def dump_flow(name: str, description: str, nodes: list, edges: list,
|
|
73
|
+
connection_names: dict[int, str] | None = None) -> str:
|
|
74
|
+
"""connection_names maps connection_id -> name so flow files stay portable."""
|
|
75
|
+
names = connection_names or {}
|
|
76
|
+
out_nodes = []
|
|
77
|
+
for n in nodes or []:
|
|
78
|
+
node = dict(n)
|
|
79
|
+
cfg = dict(node.get("config") or {})
|
|
80
|
+
cid = cfg.pop("connection_id", None)
|
|
81
|
+
if cid is not None and cid in names:
|
|
82
|
+
cfg["connection"] = names[cid]
|
|
83
|
+
node["config"] = cfg
|
|
84
|
+
out_nodes.append(node)
|
|
85
|
+
doc: dict = {"version": VERSION, "kind": "flow", "name": name or "flow"}
|
|
86
|
+
if description:
|
|
87
|
+
doc["description"] = description
|
|
88
|
+
doc["nodes"] = out_nodes
|
|
89
|
+
doc["edges"] = edges or []
|
|
90
|
+
return yaml.safe_dump(doc, sort_keys=False, allow_unicode=True, width=100)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def load(text: str) -> dict:
|
|
94
|
+
"""Parse + validate an .provekit document. Returns the normalized dict; the caller
|
|
95
|
+
resolves `connection` names to ids against its own workspace."""
|
|
96
|
+
try:
|
|
97
|
+
doc = yaml.safe_load(text)
|
|
98
|
+
except yaml.YAMLError as exc:
|
|
99
|
+
raise ValueError(f"not valid YAML: {exc}") from exc
|
|
100
|
+
if not isinstance(doc, dict):
|
|
101
|
+
raise ValueError("expected a YAML mapping at the top level")
|
|
102
|
+
if doc.get("version") != VERSION:
|
|
103
|
+
raise ValueError(f"unsupported version {doc.get('version')!r} (expected {VERSION})")
|
|
104
|
+
kind = doc.get("kind")
|
|
105
|
+
if kind not in KINDS:
|
|
106
|
+
raise ValueError(f"unsupported kind {kind!r} (expected one of {KINDS})")
|
|
107
|
+
if kind == "test":
|
|
108
|
+
req = doc.get("request")
|
|
109
|
+
if not isinstance(req, dict) or req.get("type") not in ("prompt", "tool", "agent"):
|
|
110
|
+
raise ValueError("test needs a request with type prompt | tool | agent")
|
|
111
|
+
if "api_key" in req:
|
|
112
|
+
raise ValueError("credentials do not belong in .provekit files — use a connection name")
|
|
113
|
+
for a in doc.get("assertions") or []:
|
|
114
|
+
if not isinstance(a, dict) or not a.get("type"):
|
|
115
|
+
raise ValueError("each assertion needs a type")
|
|
116
|
+
for r in doc.get("dataset") or []:
|
|
117
|
+
if not isinstance(r, dict):
|
|
118
|
+
raise ValueError("each dataset row must be a mapping")
|
|
119
|
+
else: # flow
|
|
120
|
+
if not isinstance(doc.get("nodes"), list) or not isinstance(doc.get("edges"), list):
|
|
121
|
+
raise ValueError("flow needs nodes and edges lists")
|
|
122
|
+
return doc
|