keboola-cli 0.63.4__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.
- keboola_agent_cli/__init__.py +34 -0
- keboola_agent_cli/__main__.py +5 -0
- keboola_agent_cli/_ui_dist/assets/arc-DhFYIddx.js +2 -0
- keboola_agent_cli/_ui_dist/assets/arc-DhFYIddx.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/architecture-7EHR7CIX-hNCijx_H.js +1 -0
- keboola_agent_cli/_ui_dist/assets/architectureDiagram-3BPJPVTR-C6hUlprM.js +37 -0
- keboola_agent_cli/_ui_dist/assets/architectureDiagram-3BPJPVTR-C6hUlprM.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/array-BifhSqXX.js +2 -0
- keboola_agent_cli/_ui_dist/assets/array-BifhSqXX.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/blockDiagram-GPEHLZMM-DC7qY9i4.js +133 -0
- keboola_agent_cli/_ui_dist/assets/blockDiagram-GPEHLZMM-DC7qY9i4.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/c4Diagram-AAUBKEIU-5Lh44evt.js +11 -0
- keboola_agent_cli/_ui_dist/assets/c4Diagram-AAUBKEIU-5Lh44evt.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/channel-DBMrXlxx.js +2 -0
- keboola_agent_cli/_ui_dist/assets/channel-DBMrXlxx.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-2J33WTMH-Coy82EBh.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-2J33WTMH-Coy82EBh.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-3OPIFGDE-BQC5CRHI.js +63 -0
- keboola_agent_cli/_ui_dist/assets/chunk-3OPIFGDE-BQC5CRHI.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-4BX2VUAB-DUuEt70o.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-4BX2VUAB-DUuEt70o.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-55IACEB6-BvR-6chF.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-55IACEB6-BvR-6chF.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-5ZQYHXKU-BjcTN7ul.js +3 -0
- keboola_agent_cli/_ui_dist/assets/chunk-5ZQYHXKU-BjcTN7ul.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-727SXJPM-C0zxqqRN.js +207 -0
- keboola_agent_cli/_ui_dist/assets/chunk-727SXJPM-C0zxqqRN.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-AQP2D5EJ-CXf7rIlZ.js +232 -0
- keboola_agent_cli/_ui_dist/assets/chunk-AQP2D5EJ-CXf7rIlZ.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-BSJP7CBP-Oj_FO9Q7.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-BSJP7CBP-Oj_FO9Q7.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-CSCIHK7Q-CcTsLrFc.js +124 -0
- keboola_agent_cli/_ui_dist/assets/chunk-CSCIHK7Q-CcTsLrFc.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-FMBD7UC4-FH-zLkkW.js +16 -0
- keboola_agent_cli/_ui_dist/assets/chunk-FMBD7UC4-FH-zLkkW.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-L5ZTLDWV-B1Ky_e7O.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-L5ZTLDWV-B1Ky_e7O.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-ND2GUHAM-BHz1rpbm.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-ND2GUHAM-BHz1rpbm.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-NNHCCRGN-DlpIbxXb.js +160 -0
- keboola_agent_cli/_ui_dist/assets/chunk-NNHCCRGN-DlpIbxXb.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-NZK2D7GU-tnrSoegS.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-NZK2D7GU-tnrSoegS.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-O5CBEL6O-DxxqDH0l.js +71 -0
- keboola_agent_cli/_ui_dist/assets/chunk-O5CBEL6O-DxxqDH0l.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-QZHKN3VN-CSjc2gjj.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-QZHKN3VN-CSjc2gjj.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/classDiagram-4FO5ZUOK-BuZcZu85.js +2 -0
- keboola_agent_cli/_ui_dist/assets/classDiagram-4FO5ZUOK-BuZcZu85.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/classDiagram-v2-Q7XG4LA2-BuZcZu85.js +2 -0
- keboola_agent_cli/_ui_dist/assets/classDiagram-v2-Q7XG4LA2-BuZcZu85.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/cose-bilkent-S5V4N54A-Y0L8LDMa.js +2 -0
- keboola_agent_cli/_ui_dist/assets/cose-bilkent-S5V4N54A-Y0L8LDMa.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/cytoscape.esm-C8YCVR3_.js +322 -0
- keboola_agent_cli/_ui_dist/assets/cytoscape.esm-C8YCVR3_.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/dagre-BM42HDAG-UZ-9BTqF.js +5 -0
- keboola_agent_cli/_ui_dist/assets/dagre-BM42HDAG-UZ-9BTqF.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/dagre-Bx709z4p.js +2 -0
- keboola_agent_cli/_ui_dist/assets/dagre-Bx709z4p.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/defaultLocale-C8Fc0cco.js +2 -0
- keboola_agent_cli/_ui_dist/assets/defaultLocale-C8Fc0cco.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/diagram-2AECGRRQ-DoDQ60wi.js +44 -0
- keboola_agent_cli/_ui_dist/assets/diagram-2AECGRRQ-DoDQ60wi.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/diagram-5GNKFQAL-CMGFxpUs.js +11 -0
- keboola_agent_cli/_ui_dist/assets/diagram-5GNKFQAL-CMGFxpUs.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/diagram-KO2AKTUF-1uGDa-Iu.js +4 -0
- keboola_agent_cli/_ui_dist/assets/diagram-KO2AKTUF-1uGDa-Iu.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/diagram-LMA3HP47-XtFH7B51.js +25 -0
- keboola_agent_cli/_ui_dist/assets/diagram-LMA3HP47-XtFH7B51.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/diagram-OG6HWLK6-B4_Te1T5.js +25 -0
- keboola_agent_cli/_ui_dist/assets/diagram-OG6HWLK6-B4_Te1T5.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/dist-Di6zmlv0.js +2 -0
- keboola_agent_cli/_ui_dist/assets/dist-Di6zmlv0.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/erDiagram-TEJ5UH35-NjQkrdFt.js +86 -0
- keboola_agent_cli/_ui_dist/assets/erDiagram-TEJ5UH35-NjQkrdFt.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/eventmodeling-FCH6USID-BrJMIks8.js +1 -0
- keboola_agent_cli/_ui_dist/assets/flowDiagram-I6XJVG4X-CIr8DWl7.js +163 -0
- keboola_agent_cli/_ui_dist/assets/flowDiagram-I6XJVG4X-CIr8DWl7.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/ganttDiagram-6RSMTGT7-C1VY_xbQ.js +293 -0
- keboola_agent_cli/_ui_dist/assets/ganttDiagram-6RSMTGT7-C1VY_xbQ.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/gitGraph-WXDBUCRP-COacYjo-.js +1 -0
- keboola_agent_cli/_ui_dist/assets/gitGraphDiagram-PVQCEYII-DQT8-kg2.js +107 -0
- keboola_agent_cli/_ui_dist/assets/gitGraphDiagram-PVQCEYII-DQT8-kg2.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/graphlib-B8gBHxth.js +2 -0
- keboola_agent_cli/_ui_dist/assets/graphlib-B8gBHxth.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/index-CMq50kkV.css +1 -0
- keboola_agent_cli/_ui_dist/assets/index-D8W97DAz.js +118 -0
- keboola_agent_cli/_ui_dist/assets/index-D8W97DAz.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/info-J43DQDTF-DdCTRIzU.js +1 -0
- keboola_agent_cli/_ui_dist/assets/infoDiagram-5YYISTIA-C77rsoTp.js +3 -0
- keboola_agent_cli/_ui_dist/assets/infoDiagram-5YYISTIA-C77rsoTp.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/init-D6jRqBbL.js +2 -0
- keboola_agent_cli/_ui_dist/assets/init-D6jRqBbL.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/ishikawaDiagram-YF4QCWOH-BcTbXaLy.js +71 -0
- keboola_agent_cli/_ui_dist/assets/ishikawaDiagram-YF4QCWOH-BcTbXaLy.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/journeyDiagram-JHISSGLW-BejeAJQ_.js +140 -0
- keboola_agent_cli/_ui_dist/assets/journeyDiagram-JHISSGLW-BejeAJQ_.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/kanban-definition-UN3LZRKU-BRNz_UrH.js +90 -0
- keboola_agent_cli/_ui_dist/assets/kanban-definition-UN3LZRKU-BRNz_UrH.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/katex-C4eR7coU.js +258 -0
- keboola_agent_cli/_ui_dist/assets/katex-C4eR7coU.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/line-CzAQKFbJ.js +2 -0
- keboola_agent_cli/_ui_dist/assets/line-CzAQKFbJ.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/linear-DUNFFdck.js +2 -0
- keboola_agent_cli/_ui_dist/assets/linear-DUNFFdck.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/mermaid-parser.core-CpuBOkFa.js +5 -0
- keboola_agent_cli/_ui_dist/assets/mermaid-parser.core-CpuBOkFa.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/mindmap-definition-RKZ34NQL-9EJQNjH0.js +97 -0
- keboola_agent_cli/_ui_dist/assets/mindmap-definition-RKZ34NQL-9EJQNjH0.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/ordinal-hYBb2elL.js +2 -0
- keboola_agent_cli/_ui_dist/assets/ordinal-hYBb2elL.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/packet-YPE3B663-DLiiw_B2.js +1 -0
- keboola_agent_cli/_ui_dist/assets/path-BWPyau1x.js +2 -0
- keboola_agent_cli/_ui_dist/assets/path-BWPyau1x.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/pie-LRSECV5Y-CRoO8G1g.js +1 -0
- keboola_agent_cli/_ui_dist/assets/pieDiagram-4H26LBE5-XH4cy6Cb.js +31 -0
- keboola_agent_cli/_ui_dist/assets/pieDiagram-4H26LBE5-XH4cy6Cb.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/quadrantDiagram-W4KKPZXB-fdhc93U8.js +8 -0
- keboola_agent_cli/_ui_dist/assets/quadrantDiagram-W4KKPZXB-fdhc93U8.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/radar-GUYGQ44K-DAlLVJHm.js +1 -0
- keboola_agent_cli/_ui_dist/assets/requirementDiagram-4Y6WPE33-a94eP3R9.js +85 -0
- keboola_agent_cli/_ui_dist/assets/requirementDiagram-4Y6WPE33-a94eP3R9.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/rough.esm-CSKSodPl.js +2 -0
- keboola_agent_cli/_ui_dist/assets/rough.esm-CSKSodPl.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/sankeyDiagram-5OEKKPKP-jcBa02sp.js +41 -0
- keboola_agent_cli/_ui_dist/assets/sankeyDiagram-5OEKKPKP-jcBa02sp.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/sequenceDiagram-3UESZ5HK-A5-GGM-e.js +163 -0
- keboola_agent_cli/_ui_dist/assets/sequenceDiagram-3UESZ5HK-A5-GGM-e.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/src-ZI-V_AF0.js +2 -0
- keboola_agent_cli/_ui_dist/assets/src-ZI-V_AF0.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/stateDiagram-AJRCARHV-BKAA5rqE.js +2 -0
- keboola_agent_cli/_ui_dist/assets/stateDiagram-AJRCARHV-BKAA5rqE.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/stateDiagram-v2-BHNVJYJU-DnJwJBsE.js +2 -0
- keboola_agent_cli/_ui_dist/assets/stateDiagram-v2-BHNVJYJU-DnJwJBsE.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/timeline-definition-PNZ67QCA-Cy39jp8b.js +121 -0
- keboola_agent_cli/_ui_dist/assets/timeline-definition-PNZ67QCA-Cy39jp8b.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/treeView-BLDUP644-DbLYl23-.js +1 -0
- keboola_agent_cli/_ui_dist/assets/treemap-LRROVOQU-Bp0eGlOt.js +1 -0
- keboola_agent_cli/_ui_dist/assets/vennDiagram-CIIHVFJN-BGECKubd.js +35 -0
- keboola_agent_cli/_ui_dist/assets/vennDiagram-CIIHVFJN-BGECKubd.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/wardley-L42UT6IY-D4yH4jqS.js +1 -0
- keboola_agent_cli/_ui_dist/assets/wardleyDiagram-YWT4CUSO-D6XRG3cZ.js +79 -0
- keboola_agent_cli/_ui_dist/assets/wardleyDiagram-YWT4CUSO-D6XRG3cZ.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/xychartDiagram-2RQKCTM6-DRre-pfZ.js +8 -0
- keboola_agent_cli/_ui_dist/assets/xychartDiagram-2RQKCTM6-DRre-pfZ.js.map +1 -0
- keboola_agent_cli/_ui_dist/index.html +50 -0
- keboola_agent_cli/ai_client.py +83 -0
- keboola_agent_cli/auto_update.py +550 -0
- keboola_agent_cli/changelog.py +1198 -0
- keboola_agent_cli/cli.py +448 -0
- keboola_agent_cli/client.py +3422 -0
- keboola_agent_cli/commands/__init__.py +0 -0
- keboola_agent_cli/commands/_data_app_git.py +343 -0
- keboola_agent_cli/commands/_helpers.py +377 -0
- keboola_agent_cli/commands/_metadata_input.py +49 -0
- keboola_agent_cli/commands/_semantic_layer_crud.py +632 -0
- keboola_agent_cli/commands/_semantic_layer_helpers.py +44 -0
- keboola_agent_cli/commands/_semantic_layer_reference_data.py +247 -0
- keboola_agent_cli/commands/agent.py +968 -0
- keboola_agent_cli/commands/branch.py +423 -0
- keboola_agent_cli/commands/changelog.py +168 -0
- keboola_agent_cli/commands/component.py +216 -0
- keboola_agent_cli/commands/config.py +2442 -0
- keboola_agent_cli/commands/context.py +1481 -0
- keboola_agent_cli/commands/data_app.py +1279 -0
- keboola_agent_cli/commands/dev_portal.py +584 -0
- keboola_agent_cli/commands/doctor.py +37 -0
- keboola_agent_cli/commands/encrypt.py +145 -0
- keboola_agent_cli/commands/feature.py +311 -0
- keboola_agent_cli/commands/flow.py +948 -0
- keboola_agent_cli/commands/http_client.py +157 -0
- keboola_agent_cli/commands/init.py +279 -0
- keboola_agent_cli/commands/job.py +661 -0
- keboola_agent_cli/commands/kai.py +301 -0
- keboola_agent_cli/commands/lineage.py +1464 -0
- keboola_agent_cli/commands/org.py +292 -0
- keboola_agent_cli/commands/permissions.py +360 -0
- keboola_agent_cli/commands/project.py +1192 -0
- keboola_agent_cli/commands/repl.py +243 -0
- keboola_agent_cli/commands/schedule.py +340 -0
- keboola_agent_cli/commands/search.py +178 -0
- keboola_agent_cli/commands/semantic_layer.py +939 -0
- keboola_agent_cli/commands/serve.py +272 -0
- keboola_agent_cli/commands/sharing.py +340 -0
- keboola_agent_cli/commands/storage.py +2630 -0
- keboola_agent_cli/commands/stream.py +266 -0
- keboola_agent_cli/commands/sync.py +1277 -0
- keboola_agent_cli/commands/tool.py +206 -0
- keboola_agent_cli/commands/version.py +186 -0
- keboola_agent_cli/commands/workspace.py +635 -0
- keboola_agent_cli/config_store.py +582 -0
- keboola_agent_cli/constants.py +528 -0
- keboola_agent_cli/data_science_client.py +342 -0
- keboola_agent_cli/dev_portal_client.py +323 -0
- keboola_agent_cli/errors.py +248 -0
- keboola_agent_cli/http_base.py +315 -0
- keboola_agent_cli/json_utils.py +126 -0
- keboola_agent_cli/lib.py +536 -0
- keboola_agent_cli/manage_client.py +324 -0
- keboola_agent_cli/metastore_client.py +214 -0
- keboola_agent_cli/models.py +427 -0
- keboola_agent_cli/output.py +1084 -0
- keboola_agent_cli/permissions.py +469 -0
- keboola_agent_cli/py.typed +3 -0
- keboola_agent_cli/result_models.py +271 -0
- keboola_agent_cli/server/__init__.py +34 -0
- keboola_agent_cli/server/agent_runner.py +1289 -0
- keboola_agent_cli/server/agents_store.py +325 -0
- keboola_agent_cli/server/app.py +764 -0
- keboola_agent_cli/server/auth.py +117 -0
- keboola_agent_cli/server/dependencies.py +149 -0
- keboola_agent_cli/server/pricing.py +303 -0
- keboola_agent_cli/server/routers/__init__.py +1 -0
- keboola_agent_cli/server/routers/agents.py +616 -0
- keboola_agent_cli/server/routers/ai_chat.py +129 -0
- keboola_agent_cli/server/routers/branches.py +133 -0
- keboola_agent_cli/server/routers/components.py +48 -0
- keboola_agent_cli/server/routers/configs.py +507 -0
- keboola_agent_cli/server/routers/data_apps.py +384 -0
- keboola_agent_cli/server/routers/dev_portal.py +67 -0
- keboola_agent_cli/server/routers/encrypt.py +35 -0
- keboola_agent_cli/server/routers/feature.py +179 -0
- keboola_agent_cli/server/routers/flows.py +204 -0
- keboola_agent_cli/server/routers/health.py +53 -0
- keboola_agent_cli/server/routers/jobs.py +175 -0
- keboola_agent_cli/server/routers/kai.py +80 -0
- keboola_agent_cli/server/routers/lineage.py +226 -0
- keboola_agent_cli/server/routers/mcp.py +70 -0
- keboola_agent_cli/server/routers/members.py +170 -0
- keboola_agent_cli/server/routers/org.py +96 -0
- keboola_agent_cli/server/routers/projects.py +106 -0
- keboola_agent_cli/server/routers/schedules.py +54 -0
- keboola_agent_cli/server/routers/search.py +30 -0
- keboola_agent_cli/server/routers/semantic_layer.py +650 -0
- keboola_agent_cli/server/routers/sharing.py +86 -0
- keboola_agent_cli/server/routers/storage.py +574 -0
- keboola_agent_cli/server/routers/stream.py +100 -0
- keboola_agent_cli/server/routers/workspaces.py +302 -0
- keboola_agent_cli/server/run_broadcaster.py +329 -0
- keboola_agent_cli/server/sse.py +25 -0
- keboola_agent_cli/services/__init__.py +0 -0
- keboola_agent_cli/services/_encryption.py +217 -0
- keboola_agent_cli/services/_semantic_layer_cascade.py +147 -0
- keboola_agent_cli/services/_semantic_layer_crud.py +382 -0
- keboola_agent_cli/services/_semantic_layer_internals.py +1078 -0
- keboola_agent_cli/services/_semantic_layer_lookup.py +181 -0
- keboola_agent_cli/services/_semantic_layer_reference_data.py +217 -0
- keboola_agent_cli/services/_sync_bindings.py +456 -0
- keboola_agent_cli/services/_sync_branch.py +191 -0
- keboola_agent_cli/services/_sync_bulk.py +228 -0
- keboola_agent_cli/services/_sync_clone.py +163 -0
- keboola_agent_cli/services/_sync_models.py +97 -0
- keboola_agent_cli/services/_sync_push_ops.py +369 -0
- keboola_agent_cli/services/_sync_storage.py +376 -0
- keboola_agent_cli/services/_sync_writeback.py +167 -0
- keboola_agent_cli/services/agent_service.py +458 -0
- keboola_agent_cli/services/base.py +175 -0
- keboola_agent_cli/services/branch_service.py +588 -0
- keboola_agent_cli/services/component_service.py +694 -0
- keboola_agent_cli/services/config_service.py +2099 -0
- keboola_agent_cli/services/data_app_git_service.py +224 -0
- keboola_agent_cli/services/data_app_service.py +2082 -0
- keboola_agent_cli/services/deep_lineage_service.py +1322 -0
- keboola_agent_cli/services/dev_portal_service.py +345 -0
- keboola_agent_cli/services/doctor_service.py +445 -0
- keboola_agent_cli/services/encrypt_service.py +87 -0
- keboola_agent_cli/services/feature_service.py +268 -0
- keboola_agent_cli/services/flow_service.py +769 -0
- keboola_agent_cli/services/flow_validation.py +188 -0
- keboola_agent_cli/services/http_forwarder_service.py +236 -0
- keboola_agent_cli/services/job_idempotency_store.py +285 -0
- keboola_agent_cli/services/job_service.py +797 -0
- keboola_agent_cli/services/kai_service.py +367 -0
- keboola_agent_cli/services/lineage_service.py +274 -0
- keboola_agent_cli/services/mcp_service.py +1498 -0
- keboola_agent_cli/services/mcp_transport.py +259 -0
- keboola_agent_cli/services/member_service.py +593 -0
- keboola_agent_cli/services/org_service.py +619 -0
- keboola_agent_cli/services/project_service.py +947 -0
- keboola_agent_cli/services/repo_validate_service.py +767 -0
- keboola_agent_cli/services/schedule_service.py +731 -0
- keboola_agent_cli/services/search_service.py +331 -0
- keboola_agent_cli/services/semantic_layer_service.py +1497 -0
- keboola_agent_cli/services/sharing_service.py +307 -0
- keboola_agent_cli/services/storage_service.py +2524 -0
- keboola_agent_cli/services/stream_service.py +395 -0
- keboola_agent_cli/services/sync_service.py +2244 -0
- keboola_agent_cli/services/variables_service.py +447 -0
- keboola_agent_cli/services/version_service.py +1038 -0
- keboola_agent_cli/services/workspace_service.py +1103 -0
- keboola_agent_cli/stream_client.py +217 -0
- keboola_agent_cli/sync/__init__.py +1 -0
- keboola_agent_cli/sync/branch_mapping.py +174 -0
- keboola_agent_cli/sync/clone.py +211 -0
- keboola_agent_cli/sync/code_extraction.py +655 -0
- keboola_agent_cli/sync/config_format.py +290 -0
- keboola_agent_cli/sync/diff_engine.py +566 -0
- keboola_agent_cli/sync/git_utils.py +93 -0
- keboola_agent_cli/sync/manifest.py +162 -0
- keboola_agent_cli/sync/naming.py +90 -0
- keboola_agent_cli/sync/secrets.py +62 -0
- keboola_agent_cli/sync/sql_split.py +134 -0
- keboola_cli-0.63.4.dist-info/METADATA +308 -0
- keboola_cli-0.63.4.dist-info/RECORD +306 -0
- keboola_cli-0.63.4.dist-info/WHEEL +4 -0
- keboola_cli-0.63.4.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
"""Agent-task scheduling endpoints (CRUD + manual run + history + SSE stream)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
from datetime import UTC
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
11
|
+
from fastapi.responses import StreamingResponse
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
from ...constants import AI_PROMPT_HELPER_TIMEOUT
|
|
15
|
+
from ..agent_runner import (
|
|
16
|
+
compute_next_run,
|
|
17
|
+
run_task_once,
|
|
18
|
+
stream_ai_agent_events,
|
|
19
|
+
)
|
|
20
|
+
from ..agents_store import (
|
|
21
|
+
AgentAction,
|
|
22
|
+
AgentRun,
|
|
23
|
+
AgentStore,
|
|
24
|
+
AgentTask,
|
|
25
|
+
Trigger,
|
|
26
|
+
merge_runtime_input,
|
|
27
|
+
validate_trigger,
|
|
28
|
+
)
|
|
29
|
+
from ..dependencies import ServiceRegistry, get_registry
|
|
30
|
+
|
|
31
|
+
router = APIRouter(prefix="/agents", tags=["agents"])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class _NullStore(AgentStore):
|
|
35
|
+
"""Drop-in AgentStore for one-off /test runs: all mutations are no-ops.
|
|
36
|
+
|
|
37
|
+
Extends AgentStore so run_task_once's type annotation is satisfied.
|
|
38
|
+
We pass a non-existent sentinel path; the no-op overrides never call
|
|
39
|
+
_ensure_dirs so no filesystem access happens. The path is never touched,
|
|
40
|
+
so the Unix-only ``/dev/null`` form is harmless on Windows too.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self) -> None:
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
|
|
46
|
+
super().__init__(config_dir=Path("/dev/null/_kbagent_test"))
|
|
47
|
+
|
|
48
|
+
def append_run(self, run: AgentRun) -> None:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
def upsert_task(self, task: AgentTask) -> AgentTask:
|
|
52
|
+
return task
|
|
53
|
+
|
|
54
|
+
def get_task(self, task_id: str) -> AgentTask | None:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def append_events(self, task_id: str, run_id: str, events: list[dict[str, Any]]) -> str:
|
|
58
|
+
return f"{task_id}/{run_id}.jsonl"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _validate_trigger(
|
|
62
|
+
store: Any, trigger: Trigger | None, *, owner_task_id: str | None = None
|
|
63
|
+
) -> None:
|
|
64
|
+
"""HTTP wrapper around :func:`validate_trigger`.
|
|
65
|
+
|
|
66
|
+
The core check (existence + no self-loop) lives in ``agents_store`` so
|
|
67
|
+
the CLI service can reuse it. The router translates ``ValueError`` into
|
|
68
|
+
the 422 response shape the existing tests assert on.
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
validate_trigger(store, trigger, owner_task_id=owner_task_id)
|
|
72
|
+
except ValueError as exc:
|
|
73
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class AgentTaskCreate(BaseModel):
|
|
77
|
+
name: str
|
|
78
|
+
description: str = ""
|
|
79
|
+
cron: str = "0 * * * *"
|
|
80
|
+
manual: bool = False
|
|
81
|
+
enabled: bool = True
|
|
82
|
+
action: AgentAction
|
|
83
|
+
trigger: Trigger | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class AgentTaskUpdate(BaseModel):
|
|
87
|
+
name: str | None = None
|
|
88
|
+
description: str | None = None
|
|
89
|
+
cron: str | None = None
|
|
90
|
+
manual: bool | None = None
|
|
91
|
+
enabled: bool | None = None
|
|
92
|
+
action: AgentAction | None = None
|
|
93
|
+
# Sentinel: pass an explicit JSON ``null`` to clear the trigger; omitting
|
|
94
|
+
# the key (Pydantic default-unset) means "leave as is". FastAPI maps both
|
|
95
|
+
# to ``None`` here, so we rely on ``model_fields_set`` below.
|
|
96
|
+
trigger: Trigger | None = None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _store(request: Request):
|
|
100
|
+
store = getattr(request.app.state, "agent_store", None)
|
|
101
|
+
if store is None:
|
|
102
|
+
raise HTTPException(
|
|
103
|
+
status_code=503,
|
|
104
|
+
detail="Agent scheduler is not enabled on this server.",
|
|
105
|
+
)
|
|
106
|
+
return store
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@router.get("", summary="List scheduled tasks")
|
|
110
|
+
def list_tasks(request: Request) -> dict[str, Any]:
|
|
111
|
+
"""All persisted agent tasks (cron-scheduled + manual)."""
|
|
112
|
+
store = _store(request)
|
|
113
|
+
tasks = store.load_tasks()
|
|
114
|
+
return {"tasks": [t.model_dump(mode="json") for t in tasks]}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@router.post("", summary="Create a scheduled task")
|
|
118
|
+
def create_task(body: AgentTaskCreate, request: Request) -> dict[str, Any]:
|
|
119
|
+
"""Persist a new task. `manual=true` disables cron firing -- the task
|
|
120
|
+
only runs via `POST /agents/{id}/run` or downstream `trigger` chain.
|
|
121
|
+
"""
|
|
122
|
+
store = _store(request)
|
|
123
|
+
_validate_trigger(store, body.trigger)
|
|
124
|
+
task = AgentTask(
|
|
125
|
+
name=body.name,
|
|
126
|
+
description=body.description,
|
|
127
|
+
cron=body.cron,
|
|
128
|
+
manual=body.manual,
|
|
129
|
+
enabled=body.enabled,
|
|
130
|
+
action=body.action,
|
|
131
|
+
trigger=body.trigger,
|
|
132
|
+
# Manual tasks have no future cron firing; skip the croniter call so
|
|
133
|
+
# we don't store a bogus next_run_at the UI would then display.
|
|
134
|
+
next_run_at=None if body.manual else compute_next_run(body.cron),
|
|
135
|
+
)
|
|
136
|
+
saved = store.upsert_task(task)
|
|
137
|
+
return saved.model_dump(mode="json")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@router.get("/{task_id}", summary="Fetch one task")
|
|
141
|
+
def get_task(task_id: str, request: Request) -> dict[str, Any]:
|
|
142
|
+
"""Single task by ID (404 if no such task)."""
|
|
143
|
+
store = _store(request)
|
|
144
|
+
task = store.get_task(task_id)
|
|
145
|
+
if task is None:
|
|
146
|
+
raise HTTPException(status_code=404, detail=f"Task '{task_id}' not found")
|
|
147
|
+
return task.model_dump(mode="json")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@router.patch("/{task_id}", summary="Update a task")
|
|
151
|
+
def update_task(task_id: str, body: AgentTaskUpdate, request: Request) -> dict[str, Any]:
|
|
152
|
+
"""Partial update. Omitted fields stay unchanged; `trigger: null`
|
|
153
|
+
explicitly clears the chain trigger (Pydantic `model_fields_set` is
|
|
154
|
+
used to distinguish absent vs. explicit-null).
|
|
155
|
+
"""
|
|
156
|
+
store = _store(request)
|
|
157
|
+
task = store.get_task(task_id)
|
|
158
|
+
if task is None:
|
|
159
|
+
raise HTTPException(status_code=404, detail=f"Task '{task_id}' not found")
|
|
160
|
+
if body.name is not None:
|
|
161
|
+
task.name = body.name
|
|
162
|
+
if body.description is not None:
|
|
163
|
+
task.description = body.description
|
|
164
|
+
if body.cron is not None:
|
|
165
|
+
task.cron = body.cron
|
|
166
|
+
task.next_run_at = compute_next_run(body.cron)
|
|
167
|
+
if body.manual is not None:
|
|
168
|
+
task.manual = body.manual
|
|
169
|
+
# Switching to manual nulls out next_run_at; switching back recomputes.
|
|
170
|
+
task.next_run_at = None if body.manual else compute_next_run(task.cron)
|
|
171
|
+
if body.enabled is not None:
|
|
172
|
+
task.enabled = body.enabled
|
|
173
|
+
if body.action is not None:
|
|
174
|
+
task.action = body.action
|
|
175
|
+
# ``trigger`` uses model_fields_set so we can distinguish "field absent"
|
|
176
|
+
# (leave alone) from "explicit null" (clear chain). Pydantic v2 exposes
|
|
177
|
+
# this via the model_fields_set attribute on the BaseModel instance.
|
|
178
|
+
if "trigger" in body.model_fields_set:
|
|
179
|
+
_validate_trigger(store, body.trigger, owner_task_id=task.id)
|
|
180
|
+
task.trigger = body.trigger
|
|
181
|
+
store.upsert_task(task)
|
|
182
|
+
return task.model_dump(mode="json")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@router.delete("/{task_id}", summary="Delete a task")
|
|
186
|
+
def delete_task(task_id: str, request: Request) -> dict[str, Any]:
|
|
187
|
+
"""Remove a task. Existing run history on disk is left intact (the
|
|
188
|
+
run files live under `runs/{task_id}/` and are out of band for the
|
|
189
|
+
task record itself).
|
|
190
|
+
"""
|
|
191
|
+
store = _store(request)
|
|
192
|
+
if not store.delete_task(task_id):
|
|
193
|
+
raise HTTPException(status_code=404, detail=f"Task '{task_id}' not found")
|
|
194
|
+
return {"status": "deleted", "id": task_id}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class RunNowBody(BaseModel):
|
|
198
|
+
"""Optional per-run input merged into the task's persisted action params.
|
|
199
|
+
|
|
200
|
+
Used so manual tasks (``manual=True``) can receive ad-hoc runtime input
|
|
201
|
+
— e.g. the AI Data Lab pattern where the persisted prompt says "read the
|
|
202
|
+
user's question from runtime input", and each invocation passes a
|
|
203
|
+
different question. The merge is one-shot (does not mutate the saved
|
|
204
|
+
task); the next cron / chain firing sees only the persisted params.
|
|
205
|
+
|
|
206
|
+
Per-action-type semantics:
|
|
207
|
+
- ``ai_agent``: ``runtime_input.prompt`` (string) is appended to the
|
|
208
|
+
persisted prompt as a labeled section so the AI sees both the
|
|
209
|
+
operator's static instructions and the runtime ask.
|
|
210
|
+
- ``cli_command``: ``runtime_input.argv`` (list of strings) is appended
|
|
211
|
+
to the persisted argv list.
|
|
212
|
+
- ``mcp_tool``: ``runtime_input`` (dict) is shallow-merged into the
|
|
213
|
+
persisted MCP tool input, with runtime keys winning on conflict.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
runtime_input: dict[str, Any] | None = None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@router.post("/{task_id}/run", summary="Run a task now (blocking)")
|
|
220
|
+
async def run_now(
|
|
221
|
+
task_id: str,
|
|
222
|
+
request: Request,
|
|
223
|
+
registry: ServiceRegistry = Depends(get_registry),
|
|
224
|
+
body: RunNowBody | None = None,
|
|
225
|
+
) -> dict[str, Any]:
|
|
226
|
+
"""Trigger a task immediately (does not wait for the next cron tick).
|
|
227
|
+
|
|
228
|
+
Blocking variant kept for compatibility / scripted callers.
|
|
229
|
+
UI uses ``POST /agents/{task_id}/run/stream`` for live progress + attach.
|
|
230
|
+
|
|
231
|
+
Optional ``runtime_input`` body (typically used by manual tasks) is
|
|
232
|
+
merged into the task's persisted action params for this run only —
|
|
233
|
+
e.g. the operator types an ad-hoc question into the UI, the persisted
|
|
234
|
+
prompt is preserved, and the cron-driven next firing is unaffected.
|
|
235
|
+
"""
|
|
236
|
+
store = _store(request)
|
|
237
|
+
task = store.get_task(task_id)
|
|
238
|
+
if task is None:
|
|
239
|
+
raise HTTPException(status_code=404, detail=f"Task '{task_id}' not found")
|
|
240
|
+
runtime_input = body.runtime_input if body is not None else None
|
|
241
|
+
task_for_run = merge_runtime_input(task, runtime_input)
|
|
242
|
+
run = await run_task_once(task_for_run, registry, store)
|
|
243
|
+
return run.model_dump(mode="json")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@router.post("/{task_id}/run/stream", summary="Run a task now (SSE stream)")
|
|
247
|
+
async def run_now_stream(
|
|
248
|
+
task_id: str,
|
|
249
|
+
request: Request,
|
|
250
|
+
registry: ServiceRegistry = Depends(get_registry),
|
|
251
|
+
) -> StreamingResponse:
|
|
252
|
+
"""Run the task with SSE event streaming, supporting late attach.
|
|
253
|
+
|
|
254
|
+
If a run is already in flight for this task (someone else started it),
|
|
255
|
+
we attach: replay the buffered events from the start, then tail live.
|
|
256
|
+
If no run is active, we start one. Kill-on-empty: when every consumer
|
|
257
|
+
disconnects, the runner is cancelled so we don't leak claude subprocesses.
|
|
258
|
+
|
|
259
|
+
The final ``done`` event mirrors the AgentRun record persisted to disk;
|
|
260
|
+
callers don't need to also GET ``/agents/{id}/runs`` to learn the
|
|
261
|
+
outcome (though the persistent record is available there once the run
|
|
262
|
+
finishes).
|
|
263
|
+
"""
|
|
264
|
+
store = _store(request)
|
|
265
|
+
task = store.get_task(task_id)
|
|
266
|
+
if task is None:
|
|
267
|
+
raise HTTPException(status_code=404, detail=f"Task '{task_id}' not found")
|
|
268
|
+
broadcaster = getattr(request.app.state, "run_broadcaster", None)
|
|
269
|
+
if broadcaster is None:
|
|
270
|
+
raise HTTPException(status_code=503, detail="Run broadcaster not installed.")
|
|
271
|
+
|
|
272
|
+
async def gen() -> AsyncIterator[bytes]:
|
|
273
|
+
# Lead with `init` so the client always gets a packet within the
|
|
274
|
+
# SSE handshake window (no 30s+ silence before the agent emits).
|
|
275
|
+
yield _sse(
|
|
276
|
+
"init",
|
|
277
|
+
{"task_id": task.id, "name": task.name, "action_type": task.action.type},
|
|
278
|
+
)
|
|
279
|
+
try:
|
|
280
|
+
async for evt in broadcaster.start_or_attach(task, registry, store):
|
|
281
|
+
yield _sse(evt["event"], evt["data"])
|
|
282
|
+
except Exception as exc:
|
|
283
|
+
yield _sse("done", {"status": "error", "error": str(exc)})
|
|
284
|
+
|
|
285
|
+
return StreamingResponse(
|
|
286
|
+
gen(),
|
|
287
|
+
media_type="text/event-stream",
|
|
288
|
+
headers={"Cache-Control": "no-cache, no-transform", "X-Accel-Buffering": "no"},
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@router.get("/{task_id}/runs", summary="List recent runs")
|
|
293
|
+
def list_runs(task_id: str, request: Request, limit: int = 50) -> dict[str, Any]:
|
|
294
|
+
"""Last `limit` runs (default 50) for one task, newest first."""
|
|
295
|
+
store = _store(request)
|
|
296
|
+
runs = store.list_runs(task_id, limit=limit)
|
|
297
|
+
return {"runs": [r.model_dump(mode="json") for r in runs]}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@router.get("/{task_id}/runs/{run_id}", summary="Fetch one run")
|
|
301
|
+
def get_run(task_id: str, run_id: str, request: Request) -> dict[str, Any]:
|
|
302
|
+
"""Fetch a single persisted run record by its run_id.
|
|
303
|
+
|
|
304
|
+
Lighter than ``/runs?limit=...`` when the UI already has the run_id
|
|
305
|
+
(e.g. clicking on a row from the runs list). Includes the ``summary``
|
|
306
|
+
(model, tokens, cost, tool counts) and ``events_path`` so the caller
|
|
307
|
+
knows whether a timeline can be replayed via ``/runs/{run_id}/events``.
|
|
308
|
+
"""
|
|
309
|
+
store = _store(request)
|
|
310
|
+
run = store.get_run(task_id, run_id)
|
|
311
|
+
if run is None:
|
|
312
|
+
raise HTTPException(status_code=404, detail=f"Run '{run_id}' not found")
|
|
313
|
+
return run.model_dump(mode="json")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@router.get("/{task_id}/runs/{run_id}/events", summary="Replay run event timeline")
|
|
317
|
+
def get_run_events(task_id: str, run_id: str, request: Request) -> dict[str, Any]:
|
|
318
|
+
"""Return the full event timeline for one finished run.
|
|
319
|
+
|
|
320
|
+
Used by the detail drawer to "replay" a run with the same per-step
|
|
321
|
+
UI shown during a live run. ``events`` mirrors the SSE stream shape
|
|
322
|
+
one-for-one (each item has ``event`` + ``data`` + ``seq`` keys); the
|
|
323
|
+
frontend renderer can treat live and replay sources interchangeably.
|
|
324
|
+
|
|
325
|
+
Returns 404 if the run exists but no timeline was persisted (e.g. an
|
|
326
|
+
older run from before v0.10.x), so the caller can fall back to the
|
|
327
|
+
legacy ``output.response`` rendering.
|
|
328
|
+
"""
|
|
329
|
+
store = _store(request)
|
|
330
|
+
events = store.load_events(task_id, run_id)
|
|
331
|
+
if events is None:
|
|
332
|
+
raise HTTPException(
|
|
333
|
+
status_code=404,
|
|
334
|
+
detail=f"No event timeline persisted for run '{run_id}' (likely a pre-0.10 run)",
|
|
335
|
+
)
|
|
336
|
+
return {"events": events, "count": len(events)}
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@router.post("/test", summary="Dry-run an action (no persistence)")
|
|
340
|
+
async def test_action(
|
|
341
|
+
body: AgentTaskCreate,
|
|
342
|
+
registry: ServiceRegistry = Depends(get_registry),
|
|
343
|
+
) -> dict[str, Any]:
|
|
344
|
+
"""Execute an action ad-hoc -- no persistence, no scheduling.
|
|
345
|
+
|
|
346
|
+
Used by the React "Run preview" button so users can validate an action
|
|
347
|
+
before saving the task. The result mirrors what a real run would
|
|
348
|
+
produce, but nothing is written to ``agents.json`` or run history.
|
|
349
|
+
"""
|
|
350
|
+
# Build a transient task. Reuse run_task_once so the dispatch logic
|
|
351
|
+
# (mcp_tool / cli_command / ai_agent) is the same code path that
|
|
352
|
+
# the scheduler uses -- prevents test-time and live-time divergence.
|
|
353
|
+
transient = AgentTask(
|
|
354
|
+
name=body.name or "[preview]",
|
|
355
|
+
description=body.description,
|
|
356
|
+
cron=body.cron,
|
|
357
|
+
enabled=False,
|
|
358
|
+
action=body.action,
|
|
359
|
+
)
|
|
360
|
+
# Use a throwaway in-memory store so run_task_once's persistence side
|
|
361
|
+
# effects (append_run, upsert_task) write to /dev/null.
|
|
362
|
+
store = _NullStore()
|
|
363
|
+
run: AgentRun = await run_task_once(transient, registry, store)
|
|
364
|
+
return run.model_dump(mode="json")
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _sse(event: str, data: Any) -> bytes:
|
|
368
|
+
"""Format one SSE message: `event: <name>\\ndata: <json>\\n\\n`."""
|
|
369
|
+
payload = json.dumps(data, ensure_ascii=False)
|
|
370
|
+
return f"event: {event}\ndata: {payload}\n\n".encode()
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@router.post("/test/stream", summary="Dry-run an action (SSE stream)")
|
|
374
|
+
async def test_action_stream(
|
|
375
|
+
body: AgentTaskCreate,
|
|
376
|
+
registry: ServiceRegistry = Depends(get_registry),
|
|
377
|
+
) -> StreamingResponse:
|
|
378
|
+
"""Stream a test-run as SSE.
|
|
379
|
+
|
|
380
|
+
AI-agent runs (action.type == "ai_agent") emit one SSE event per line of
|
|
381
|
+
claude/codex/gemini stdout (claude is JSONL via ``--output-format=stream-json``;
|
|
382
|
+
codex / gemini emit raw text). Stderr is interleaved as ``stderr`` events.
|
|
383
|
+
A final ``done`` event carries exit_code, elapsed_seconds, response_text,
|
|
384
|
+
and full stderr -- so even if the client missed earlier events, the
|
|
385
|
+
last one is self-contained.
|
|
386
|
+
|
|
387
|
+
Non-streaming action types (``cli_command``, ``mcp_tool``) are wrapped:
|
|
388
|
+
the full result is emitted as a single ``done`` event. This keeps the
|
|
389
|
+
frontend code path uniform (always `/agents/test/stream`).
|
|
390
|
+
"""
|
|
391
|
+
action_type = body.action.type
|
|
392
|
+
|
|
393
|
+
async def gen() -> AsyncIterator[bytes]:
|
|
394
|
+
# Always lead with an init event so the client knows the request
|
|
395
|
+
# made it through (no 30-second silence if the AI takes a while
|
|
396
|
+
# to start producing output).
|
|
397
|
+
yield _sse(
|
|
398
|
+
"init",
|
|
399
|
+
{
|
|
400
|
+
"name": body.name or "[preview]",
|
|
401
|
+
"action_type": action_type,
|
|
402
|
+
"cron": body.cron,
|
|
403
|
+
},
|
|
404
|
+
)
|
|
405
|
+
if action_type == "ai_agent":
|
|
406
|
+
try:
|
|
407
|
+
async for evt in stream_ai_agent_events(registry, body.action.params):
|
|
408
|
+
yield _sse(evt["event"], evt["data"])
|
|
409
|
+
except Exception as exc:
|
|
410
|
+
yield _sse(
|
|
411
|
+
"done",
|
|
412
|
+
{"status": "error", "error": str(exc)},
|
|
413
|
+
)
|
|
414
|
+
else:
|
|
415
|
+
# Reuse the existing non-stream dispatch for the other two
|
|
416
|
+
# action types -- they emit no incremental events anyway.
|
|
417
|
+
transient = AgentTask(
|
|
418
|
+
name=body.name or "[preview]",
|
|
419
|
+
description=body.description,
|
|
420
|
+
cron=body.cron,
|
|
421
|
+
enabled=False,
|
|
422
|
+
action=body.action,
|
|
423
|
+
)
|
|
424
|
+
try:
|
|
425
|
+
run = await run_task_once(transient, registry, _NullStore())
|
|
426
|
+
yield _sse("done", run.model_dump(mode="json"))
|
|
427
|
+
except Exception as exc:
|
|
428
|
+
yield _sse("done", {"status": "error", "error": str(exc)})
|
|
429
|
+
|
|
430
|
+
return StreamingResponse(
|
|
431
|
+
gen(),
|
|
432
|
+
media_type="text/event-stream",
|
|
433
|
+
headers={
|
|
434
|
+
# Disable proxy buffering so events flush to the client
|
|
435
|
+
# immediately (matches how the BFF's streamSSE helper
|
|
436
|
+
# already passes through these headers).
|
|
437
|
+
"Cache-Control": "no-cache, no-transform",
|
|
438
|
+
"X-Accel-Buffering": "no",
|
|
439
|
+
},
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
@router.get("/cron/preview", summary="Preview cron firings")
|
|
444
|
+
def cron_preview(cron: str, count: int = 5) -> dict[str, Any]:
|
|
445
|
+
"""Preview the next ``count`` firings of a cron expression. Validates syntax."""
|
|
446
|
+
from datetime import datetime
|
|
447
|
+
|
|
448
|
+
from croniter import croniter
|
|
449
|
+
|
|
450
|
+
try:
|
|
451
|
+
it = croniter(cron, datetime.now(UTC))
|
|
452
|
+
firings = []
|
|
453
|
+
for _ in range(max(1, min(count, 20))):
|
|
454
|
+
dt = it.get_next(datetime)
|
|
455
|
+
if dt.tzinfo is None:
|
|
456
|
+
dt = dt.replace(tzinfo=UTC)
|
|
457
|
+
firings.append(dt.isoformat())
|
|
458
|
+
return {"cron": cron, "firings": firings}
|
|
459
|
+
except Exception as exc:
|
|
460
|
+
raise HTTPException(status_code=400, detail=f"Invalid cron expression: {exc}") from exc
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
class PromptHelperRequest(BaseModel):
|
|
464
|
+
"""Input for the /agents/prompt/improve/stream endpoint.
|
|
465
|
+
|
|
466
|
+
The helper rewrites the user's plain-English goal (and any half-baked
|
|
467
|
+
draft) into a polished prompt suitable for an AI-agent scheduled task.
|
|
468
|
+
The output is streamed back as SSE so the UI can show progress, and the
|
|
469
|
+
final ``done`` event carries the cleaned prompt body ready to drop into
|
|
470
|
+
the task's prompt textarea.
|
|
471
|
+
"""
|
|
472
|
+
|
|
473
|
+
cli: str # claude | codex | gemini -- same recipe as ai_agent runs
|
|
474
|
+
goal: str
|
|
475
|
+
draft: str = ""
|
|
476
|
+
project: str | None = None
|
|
477
|
+
extra_args: list[str] = []
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
@router.post("/prompt/improve", summary="AI-rewrite a prompt (blocking)")
|
|
481
|
+
async def improve_prompt(
|
|
482
|
+
body: PromptHelperRequest,
|
|
483
|
+
registry: ServiceRegistry = Depends(get_registry),
|
|
484
|
+
) -> dict[str, Any]:
|
|
485
|
+
"""Blocking variant of ``/prompt/improve/stream`` -- consumes the SSE
|
|
486
|
+
generator server-side and returns the final ``done`` payload.
|
|
487
|
+
|
|
488
|
+
Drives the same ``stream_ai_agent_events`` machinery the streaming
|
|
489
|
+
variant uses, so the resulting ``data.prompt`` is byte-for-byte
|
|
490
|
+
identical to what the SSE consumer would see in its last event.
|
|
491
|
+
Useful for scripted callers (``kbagent http`` from inside a scheduled
|
|
492
|
+
task, batch wrappers) where opening an SSE connection just to read
|
|
493
|
+
the final frame is needless ceremony.
|
|
494
|
+
|
|
495
|
+
Returns the enriched ``done.data`` payload directly: ``{status,
|
|
496
|
+
exit_code, elapsed_seconds, response, prompt, raw_response, ...}``.
|
|
497
|
+
The CLI's ``kbagent agent prompt-improve --no-stream`` consumes this
|
|
498
|
+
endpoint's shape verbatim.
|
|
499
|
+
"""
|
|
500
|
+
from ..agent_runner import (
|
|
501
|
+
build_prompt_helper_meta_prompt,
|
|
502
|
+
clean_prompt_helper_response,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
goal = body.goal.strip()
|
|
506
|
+
if not goal:
|
|
507
|
+
raise HTTPException(status_code=400, detail="goal must not be empty")
|
|
508
|
+
meta_prompt = build_prompt_helper_meta_prompt(
|
|
509
|
+
goal=goal,
|
|
510
|
+
draft=body.draft,
|
|
511
|
+
project=body.project,
|
|
512
|
+
)
|
|
513
|
+
params: dict[str, Any] = {
|
|
514
|
+
"cli": body.cli,
|
|
515
|
+
"prompt": meta_prompt,
|
|
516
|
+
"extra_args": body.extra_args,
|
|
517
|
+
"timeout": AI_PROMPT_HELPER_TIMEOUT,
|
|
518
|
+
}
|
|
519
|
+
final_done: dict[str, Any] | None = None
|
|
520
|
+
try:
|
|
521
|
+
async for evt in stream_ai_agent_events(registry, params):
|
|
522
|
+
if evt["event"] == "done":
|
|
523
|
+
raw = str(evt["data"].get("response") or "")
|
|
524
|
+
final_done = {
|
|
525
|
+
**evt["data"],
|
|
526
|
+
"prompt": clean_prompt_helper_response(raw),
|
|
527
|
+
"raw_response": raw,
|
|
528
|
+
}
|
|
529
|
+
except ValueError as exc:
|
|
530
|
+
# Same translation the streaming variant uses (bad CLI, empty
|
|
531
|
+
# prompt, malformed extra_args). 400 surfaces it cleanly to the
|
|
532
|
+
# caller without leaking a stack trace.
|
|
533
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
534
|
+
if final_done is None:
|
|
535
|
+
raise HTTPException(
|
|
536
|
+
status_code=502,
|
|
537
|
+
detail="ai_agent stream ended without a done event",
|
|
538
|
+
)
|
|
539
|
+
return final_done
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
@router.post("/prompt/improve/stream", summary="AI-rewrite a prompt (SSE stream)")
|
|
543
|
+
async def improve_prompt_stream(
|
|
544
|
+
body: PromptHelperRequest,
|
|
545
|
+
registry: ServiceRegistry = Depends(get_registry),
|
|
546
|
+
) -> StreamingResponse:
|
|
547
|
+
"""Stream an AI-generated, polished prompt back to the UI as SSE events.
|
|
548
|
+
|
|
549
|
+
The chosen CLI (claude / codex / gemini) is invoked exactly the same way
|
|
550
|
+
a scheduled ai_agent run would invoke it -- via
|
|
551
|
+
:func:`stream_ai_agent_events` -- but the *prompt* it receives is a
|
|
552
|
+
meta-prompt asking it to rewrite the user's draft into a polished
|
|
553
|
+
single-shot prompt. The UI consumes the same SSE event shapes
|
|
554
|
+
(``init`` / ``stdout`` / ``stderr`` / ``done``) the test-stream
|
|
555
|
+
endpoint emits, so it can reuse the live-progress renderer.
|
|
556
|
+
|
|
557
|
+
The ``done`` event is enriched with a ``prompt`` field carrying the
|
|
558
|
+
cleaned AI response (code fences and "Here is the prompt:" preambles
|
|
559
|
+
stripped) so the frontend can drop it straight into the textarea.
|
|
560
|
+
"""
|
|
561
|
+
from ..agent_runner import (
|
|
562
|
+
build_prompt_helper_meta_prompt,
|
|
563
|
+
clean_prompt_helper_response,
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
goal = body.goal.strip()
|
|
567
|
+
if not goal:
|
|
568
|
+
raise HTTPException(status_code=400, detail="goal must not be empty")
|
|
569
|
+
meta_prompt = build_prompt_helper_meta_prompt(
|
|
570
|
+
goal=goal,
|
|
571
|
+
draft=body.draft,
|
|
572
|
+
project=body.project,
|
|
573
|
+
)
|
|
574
|
+
params: dict[str, Any] = {
|
|
575
|
+
"cli": body.cli,
|
|
576
|
+
"prompt": meta_prompt,
|
|
577
|
+
"extra_args": body.extra_args,
|
|
578
|
+
"timeout": AI_PROMPT_HELPER_TIMEOUT,
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async def gen() -> AsyncIterator[bytes]:
|
|
582
|
+
# Mirror the /test/stream init shape so the React side can reuse
|
|
583
|
+
# AgentRunView verbatim.
|
|
584
|
+
yield _sse(
|
|
585
|
+
"init",
|
|
586
|
+
{
|
|
587
|
+
"kind": "prompt_helper",
|
|
588
|
+
"cli": body.cli,
|
|
589
|
+
"goal_preview": goal[:200],
|
|
590
|
+
},
|
|
591
|
+
)
|
|
592
|
+
try:
|
|
593
|
+
async for evt in stream_ai_agent_events(registry, params):
|
|
594
|
+
if evt["event"] == "done":
|
|
595
|
+
raw = str(evt["data"].get("response") or "")
|
|
596
|
+
cleaned = clean_prompt_helper_response(raw)
|
|
597
|
+
enriched = {**evt["data"], "prompt": cleaned, "raw_response": raw}
|
|
598
|
+
yield _sse("done", enriched)
|
|
599
|
+
else:
|
|
600
|
+
yield _sse(evt["event"], evt["data"])
|
|
601
|
+
except ValueError as exc:
|
|
602
|
+
# build_*_meta_prompt and stream_ai_agent_events raise ValueError
|
|
603
|
+
# for bad CLI / empty prompt / malformed extra_args. Send a final
|
|
604
|
+
# done event so the client doesn't hang waiting.
|
|
605
|
+
yield _sse("done", {"status": "error", "error": str(exc)})
|
|
606
|
+
except Exception as exc:
|
|
607
|
+
yield _sse("done", {"status": "error", "error": str(exc)})
|
|
608
|
+
|
|
609
|
+
return StreamingResponse(
|
|
610
|
+
gen(),
|
|
611
|
+
media_type="text/event-stream",
|
|
612
|
+
headers={
|
|
613
|
+
"Cache-Control": "no-cache, no-transform",
|
|
614
|
+
"X-Accel-Buffering": "no",
|
|
615
|
+
},
|
|
616
|
+
)
|