agentengine-sdk-python 0.6.2__py3-none-any.whl → 0.6.9__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.
- agentengine_sdk_python-0.6.9.dist-info/METADATA +179 -0
- agentengine_sdk_python-0.6.9.dist-info/RECORD +487 -0
- {agentengine_sdk_python-0.6.2.dist-info → agentengine_sdk_python-0.6.9.dist-info}/WHEEL +1 -1
- agentengine_sdk_python-0.6.9.dist-info/entry_points.txt +3 -0
- agentengine_sdk_python-0.6.9.dist-info/licenses/LICENSE +175 -0
- agentengine_sdk_python-0.6.9.dist-info/top_level.txt +2 -0
- ksadk/__init__.py +13 -0
- ksadk/__main__.py +4 -0
- ksadk/a2a/__init__.py +15 -0
- ksadk/a2a/card_builder.py +64 -0
- ksadk/a2a/client.py +309 -0
- ksadk/a2a/executor.py +201 -0
- ksadk/a2a/server.py +79 -0
- ksadk/agents/__init__.py +20 -0
- ksadk/agents/base.py +370 -0
- ksadk/agents/context.py +38 -0
- ksadk/agents/event.py +35 -0
- ksadk/agents/loop_agent.py +71 -0
- ksadk/agents/parallel_agent.py +109 -0
- ksadk/agents/runner_adapter.py +24 -0
- ksadk/agents/sequential_agent.py +34 -0
- ksadk/api/__init__.py +3 -0
- ksadk/api/client.py +2227 -0
- ksadk/builders/__init__.py +20 -0
- ksadk/builders/base.py +66 -0
- ksadk/builders/code_builder.py +1990 -0
- ksadk/builders/container_builder.py +786 -0
- ksadk/builders/framework_requirements.py +59 -0
- ksadk/builders/ks3_uploader.py +389 -0
- ksadk/builders/mcp_builder.py +476 -0
- ksadk/builders/requirements_utils.py +75 -0
- ksadk/cli/__init__.py +531 -0
- ksadk/cli/__main__.py +4 -0
- ksadk/cli/agent_ref.py +204 -0
- ksadk/cli/cmd_a2a.py +167 -0
- ksadk/cli/cmd_agent.py +201 -0
- ksadk/cli/cmd_build.py +270 -0
- ksadk/cli/cmd_completion.py +304 -0
- ksadk/cli/cmd_config.py +758 -0
- ksadk/cli/cmd_create.py +2277 -0
- ksadk/cli/cmd_dashboard.py +900 -0
- ksadk/cli/cmd_deploy.py +704 -0
- ksadk/cli/cmd_destroy.py +345 -0
- ksadk/cli/cmd_files.py +1227 -0
- ksadk/cli/cmd_hermes.py +1259 -0
- ksadk/cli/cmd_invoke.py +1336 -0
- ksadk/cli/cmd_launch.py +564 -0
- ksadk/cli/cmd_mcp.py +1174 -0
- ksadk/cli/cmd_model.py +302 -0
- ksadk/cli/cmd_openclaw.py +3918 -0
- ksadk/cli/cmd_run.py +232 -0
- ksadk/cli/cmd_status.py +588 -0
- ksadk/cli/cmd_version.py +522 -0
- ksadk/cli/cmd_web.py +288 -0
- ksadk/cli/deploy_utils.py +87 -0
- ksadk/cli/dry_run.py +214 -0
- ksadk/cli/env_options.py +82 -0
- ksadk/cli/error_utils.py +525 -0
- ksadk/cli/global_options.py +31 -0
- ksadk/cli/local_runtime.py +97 -0
- ksadk/cli/model_catalog.py +136 -0
- ksadk/cli/network_options.py +410 -0
- ksadk/cli/resource_common.py +621 -0
- ksadk/cli/storage.py +84 -0
- ksadk/cli/ui.py +431 -0
- ksadk/cli/workflow_common.py +552 -0
- ksadk/common/__init__.py +14 -0
- ksadk/common/aicp_env.py +128 -0
- ksadk/common/auth.py +190 -0
- ksadk/common/constants.py +84 -0
- ksadk/common/llm_utils.py +61 -0
- ksadk/configs/__init__.py +61 -0
- ksadk/configs/env_registry.py +342 -0
- ksadk/configs/global_config.py +200 -0
- ksadk/configs/settings.py +706 -0
- ksadk/conversations/__init__.py +95 -0
- ksadk/conversations/attachment_storage.py +379 -0
- ksadk/conversations/attachments.py +774 -0
- ksadk/conversations/compaction_pipeline.py +387 -0
- ksadk/conversations/compaction_prompt.py +128 -0
- ksadk/conversations/context.py +303 -0
- ksadk/conversations/model_context.py +360 -0
- ksadk/conversations/model_options.py +136 -0
- ksadk/conversations/normalize.py +446 -0
- ksadk/conversations/reasoning_markup.py +91 -0
- ksadk/conversations/run_kinds.py +61 -0
- ksadk/conversations/run_status.py +53 -0
- ksadk/conversations/runtime.py +5208 -0
- ksadk/conversations/semantic_summary.py +359 -0
- ksadk/conversations/session_title.py +254 -0
- ksadk/deployment/__init__.py +83 -0
- ksadk/deployment/agent_access.py +226 -0
- ksadk/deployment/base.py +229 -0
- ksadk/deployment/manager.py +332 -0
- ksadk/deployment/providers/__init__.py +10 -0
- ksadk/deployment/providers/serverless.py +1105 -0
- ksadk/deployment/registry.py +120 -0
- ksadk/deployment/state.py +78 -0
- ksadk/deployment/ui_config.py +21 -0
- ksadk/detection/__init__.py +7 -0
- ksadk/detection/detector.py +431 -0
- ksadk/detection/mcp_detector.py +250 -0
- ksadk/hermes_terminal.py +467 -0
- ksadk/identity/__init__.py +17 -0
- ksadk/identity/resolver.py +385 -0
- ksadk/knowledge_base/__init__.py +62 -0
- ksadk/knowledge_base/adk_tool.py +47 -0
- ksadk/knowledge_base/client.py +297 -0
- ksadk/knowledge_base/langchain_tool.py +55 -0
- ksadk/knowledge_base/service.py +66 -0
- ksadk/knowledge_base/tool.py +50 -0
- ksadk/markdown.py +169 -0
- ksadk/mcp_runtime/__init__.py +231 -0
- ksadk/memory/__init__.py +57 -0
- ksadk/memory/adk/__init__.py +51 -0
- ksadk/memory/adk/backends/__init__.py +5 -0
- ksadk/memory/adk/backends/base_ltm_backend.py +52 -0
- ksadk/memory/adk/backends/http_ltm_backend.py +183 -0
- ksadk/memory/adk/backends/inmemory_ltm_backend.py +96 -0
- ksadk/memory/adk/backends/sdk_ltm_backend.py +456 -0
- ksadk/memory/adk/long_term_memory.py +273 -0
- ksadk/memory/adk/short_term_memory.py +289 -0
- ksadk/memory/adk_tool.py +31 -0
- ksadk/memory/backends/__init__.py +6 -0
- ksadk/memory/backends/base.py +103 -0
- ksadk/memory/backends/memory.py +112 -0
- ksadk/memory/backends/redis.py +146 -0
- ksadk/memory/langchain_tool.py +39 -0
- ksadk/memory/ltm_backend_factory.py +24 -0
- ksadk/memory/manager.py +215 -0
- ksadk/memory/service.py +172 -0
- ksadk/memory/tool.py +96 -0
- ksadk/model_policy.py +223 -0
- ksadk/openclaw_gateway.py +378 -0
- ksadk/runners/__init__.py +8 -0
- ksadk/runners/adk_runner.py +1135 -0
- ksadk/runners/base_runner.py +305 -0
- ksadk/runners/deepagents_runner.py +13 -0
- ksadk/runners/factory.py +73 -0
- ksadk/runners/langchain_runner.py +539 -0
- ksadk/runners/langgraph_runner.py +852 -0
- ksadk/runners/patch_langchain.py +187 -0
- ksadk/runners/remote_runner.py +667 -0
- ksadk/runners/usage_accumulator.py +38 -0
- ksadk/runners/utils/__init__.py +19 -0
- ksadk/runners/utils/langfuse.py +230 -0
- ksadk/runners/utils/loader.py +62 -0
- ksadk/runtime_context.py +166 -0
- ksadk/runtime_state.py +25 -0
- ksadk/sandbox/__init__.py +28 -0
- ksadk/sandbox/backends/__init__.py +9 -0
- ksadk/sandbox/backends/e2b.py +154 -0
- ksadk/sandbox/backends/local_process.py +131 -0
- ksadk/sandbox/base.py +101 -0
- ksadk/sandbox/factory.py +53 -0
- ksadk/sandbox/registry.py +187 -0
- ksadk/server/__init__.py +7 -0
- ksadk/server/api_models.py +73 -0
- ksadk/server/app.py +3789 -0
- ksadk/server/static/assets/ArtifactsPanel-C0meuOdi.js +1 -0
- ksadk/server/static/assets/CodeBlock-BbI4HPtJ.js +9 -0
- ksadk/server/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- ksadk/server/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- ksadk/server/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- ksadk/server/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- ksadk/server/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- ksadk/server/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- ksadk/server/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- ksadk/server/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- ksadk/server/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- ksadk/server/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- ksadk/server/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- ksadk/server/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- ksadk/server/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- ksadk/server/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- ksadk/server/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- ksadk/server/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- ksadk/server/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- ksadk/server/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- ksadk/server/static/assets/MathMessageMarkdown-DMLQYoLp.js +8 -0
- ksadk/server/static/assets/MathMessageMarkdown-DeYh8QPr.css +1 -0
- ksadk/server/static/assets/MermaidBlock-DCR8WZ17.js +303 -0
- ksadk/server/static/assets/NativeTerminalPanel-CHOd7Rch.css +1 -0
- ksadk/server/static/assets/NativeTerminalPanel-CKUvWhUz.js +1 -0
- ksadk/server/static/assets/addon-fit-DthTIhi3.js +1 -0
- ksadk/server/static/assets/apl-CnwPGSsG.js +1 -0
- ksadk/server/static/assets/arc-bj2rZnP5.js +1 -0
- ksadk/server/static/assets/architecture-7EHR7CIX-Cr6Xz1_c.js +1 -0
- ksadk/server/static/assets/architectureDiagram-3BPJPVTR-2u1ltDIj.js +36 -0
- ksadk/server/static/assets/asciiarmor-qTkVPQu6.js +1 -0
- ksadk/server/static/assets/asn1-Dr8qZg38.js +1 -0
- ksadk/server/static/assets/asterisk-Bja0s6e1.js +1 -0
- ksadk/server/static/assets/blockDiagram-GPEHLZMM-D6fvdK20.js +132 -0
- ksadk/server/static/assets/brainfuck-D5EjA2JK.js +1 -0
- ksadk/server/static/assets/c4Diagram-AAUBKEIU-BK82L5n5.js +10 -0
- ksadk/server/static/assets/channel-BSSOwFIb.js +1 -0
- ksadk/server/static/assets/chunk-2J33WTMH-DLVdkeyQ.js +1 -0
- ksadk/server/static/assets/chunk-4BX2VUAB-BbwsYcbW.js +1 -0
- ksadk/server/static/assets/chunk-55IACEB6-DOairxzw.js +1 -0
- ksadk/server/static/assets/chunk-727SXJPM-hCO487gs.js +206 -0
- ksadk/server/static/assets/chunk-AQP2D5EJ-XU8vDOuG.js +231 -0
- ksadk/server/static/assets/chunk-FMBD7UC4-PrapyfMu.js +15 -0
- ksadk/server/static/assets/chunk-ND2GUHAM-CboKhTZ3.js +1 -0
- ksadk/server/static/assets/chunk-QZHKN3VN-XXpdiwUL.js +1 -0
- ksadk/server/static/assets/classDiagram-4FO5ZUOK-C5HvGiyK.js +1 -0
- ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-C5HvGiyK.js +1 -0
- ksadk/server/static/assets/clike-DHH8Ad3s.js +1 -0
- ksadk/server/static/assets/clojure-BflJGmDX.js +1 -0
- ksadk/server/static/assets/cmake-CyVbuzPu.js +1 -0
- ksadk/server/static/assets/cobol-B_OZ4V-R.js +1 -0
- ksadk/server/static/assets/coffeescript-B2pKbFk8.js +1 -0
- ksadk/server/static/assets/commonlisp-CcllspGY.js +1 -0
- ksadk/server/static/assets/cose-bilkent-S5V4N54A-_N8bJ-EU.js +1 -0
- ksadk/server/static/assets/crystal-M5-qzICs.js +1 -0
- ksadk/server/static/assets/css-CYpP4FRV.js +1 -0
- ksadk/server/static/assets/cypher-p9eYesGn.js +1 -0
- ksadk/server/static/assets/cytoscape.esm-DelgaX4f.js +321 -0
- ksadk/server/static/assets/d-BhMjBjQP.js +1 -0
- ksadk/server/static/assets/dagre-BM42HDAG-FoDlo5J2.js +4 -0
- ksadk/server/static/assets/dagre-Bx709z4p.js +1 -0
- ksadk/server/static/assets/defaultLocale-C8Fc0cco.js +1 -0
- ksadk/server/static/assets/diagram-2AECGRRQ-BBp6ebBw.js +43 -0
- ksadk/server/static/assets/diagram-5GNKFQAL-CqOvG1PR.js +10 -0
- ksadk/server/static/assets/diagram-KO2AKTUF-O9T3t6ba.js +3 -0
- ksadk/server/static/assets/diagram-LMA3HP47-CGG8ekBO.js +24 -0
- ksadk/server/static/assets/diagram-OG6HWLK6-4BmH1EEg.js +24 -0
- ksadk/server/static/assets/diff-ChtP43wD.js +1 -0
- ksadk/server/static/assets/dist-6kSZvCZh.js +1 -0
- ksadk/server/static/assets/dist-B67uF2uK.js +1 -0
- ksadk/server/static/assets/dist-BMKM66B4.js +1 -0
- ksadk/server/static/assets/dist-BVzcWa6D.js +1 -0
- ksadk/server/static/assets/dist-Bk1l05Ni.js +1 -0
- ksadk/server/static/assets/dist-BkJjNih3.js +1 -0
- ksadk/server/static/assets/dist-BmuTWcm7.js +9 -0
- ksadk/server/static/assets/dist-C2DKRFVK.js +1 -0
- ksadk/server/static/assets/dist-CWiTDN_V.js +6 -0
- ksadk/server/static/assets/dist-CZ6C3x8B.js +23 -0
- ksadk/server/static/assets/dist-CiZHB6ZW.js +1 -0
- ksadk/server/static/assets/dist-Cp1_jKwf.js +1 -0
- ksadk/server/static/assets/dist-Cwhk4lwI.js +2 -0
- ksadk/server/static/assets/dist-D5JGcxBx.js +11 -0
- ksadk/server/static/assets/dist-D99wf6_X.js +1 -0
- ksadk/server/static/assets/dist-DCpZrjaK.js +1 -0
- ksadk/server/static/assets/dist-DI-oVAXw.js +1 -0
- ksadk/server/static/assets/dist-DLMjL01k.js +1 -0
- ksadk/server/static/assets/dist-DLeM1OCK.js +1 -0
- ksadk/server/static/assets/dist-DM0rEQPk.js +1 -0
- ksadk/server/static/assets/dist-DbaH3p5h.js +1 -0
- ksadk/server/static/assets/dist-S4klkK5W.js +1 -0
- ksadk/server/static/assets/dockerfile-BLkNEvjs.js +1 -0
- ksadk/server/static/assets/dtd-DTF72YNO.js +1 -0
- ksadk/server/static/assets/dylan-C6jEEEk-.js +1 -0
- ksadk/server/static/assets/ebnf-D4c0_ac3.js +1 -0
- ksadk/server/static/assets/ecl-BSZaIHnp.js +1 -0
- ksadk/server/static/assets/eiffel-BEjRio4Q.js +1 -0
- ksadk/server/static/assets/elm-Cshzl8qu.js +1 -0
- ksadk/server/static/assets/erDiagram-TEJ5UH35-oJhaci7S.js +85 -0
- ksadk/server/static/assets/erlang-DaB2Rkuy.js +1 -0
- ksadk/server/static/assets/eventmodeling-FCH6USID-CIgwHJLL.js +1 -0
- ksadk/server/static/assets/factor-D7LVTn2l.js +1 -0
- ksadk/server/static/assets/fcl-ClOhbFR7.js +1 -0
- ksadk/server/static/assets/flowDiagram-I6XJVG4X-DtTmUan-.js +162 -0
- ksadk/server/static/assets/forth-Cezjo90N.js +1 -0
- ksadk/server/static/assets/fortran-Bt6PBEDR.js +1 -0
- ksadk/server/static/assets/ganttDiagram-6RSMTGT7-BSsb_eij.js +292 -0
- ksadk/server/static/assets/gas-BHEdbvp9.js +1 -0
- ksadk/server/static/assets/gherkin-oBAE_ms0.js +1 -0
- ksadk/server/static/assets/gitGraph-WXDBUCRP-BVAJ7uo0.js +1 -0
- ksadk/server/static/assets/gitGraphDiagram-PVQCEYII-BR5s6Pws.js +106 -0
- ksadk/server/static/assets/graphlib-B8gBHxth.js +1 -0
- ksadk/server/static/assets/groovy-BC3IOsC8.js +1 -0
- ksadk/server/static/assets/haskell-BRsoo5mP.js +1 -0
- ksadk/server/static/assets/haxe-DIz0ZZqd.js +1 -0
- ksadk/server/static/assets/http-CLVgA2GD.js +1 -0
- ksadk/server/static/assets/idl-BUZw3wgd.js +1 -0
- ksadk/server/static/assets/index-BH7-pC0B.js +165 -0
- ksadk/server/static/assets/index-DAURbCsP.css +1 -0
- ksadk/server/static/assets/info-J43DQDTF-MT80V4ge.js +1 -0
- ksadk/server/static/assets/infoDiagram-5YYISTIA-DYPc5cvh.js +2 -0
- ksadk/server/static/assets/init-D6jRqBbL.js +1 -0
- ksadk/server/static/assets/ishikawaDiagram-YF4QCWOH-MoBdALDg.js +70 -0
- ksadk/server/static/assets/javascript-9Tg8ixDm.js +1 -0
- ksadk/server/static/assets/journeyDiagram-JHISSGLW-DgLiQT8l.js +139 -0
- ksadk/server/static/assets/julia-CQfgDRfP.js +1 -0
- ksadk/server/static/assets/kanban-definition-UN3LZRKU-CsFx112F.js +89 -0
- ksadk/server/static/assets/katex-DRsiwpM2.js +265 -0
- ksadk/server/static/assets/linear-TNRQ5Orv.js +1 -0
- ksadk/server/static/assets/livescript-BXxG0Yva.js +1 -0
- ksadk/server/static/assets/lua-CiA1ziua.js +1 -0
- ksadk/server/static/assets/mathematica-u8YMmbU0.js +1 -0
- ksadk/server/static/assets/mbox-BEMVcqjs.js +1 -0
- ksadk/server/static/assets/mermaid-parser.core-D2eMVOgo.js +161 -0
- ksadk/server/static/assets/mindmap-definition-RKZ34NQL-BtByYFhL.js +96 -0
- ksadk/server/static/assets/mirc-C9zpzAZU.js +1 -0
- ksadk/server/static/assets/mllike-B45xgp2S.js +1 -0
- ksadk/server/static/assets/modelica-CqHI_q-D.js +1 -0
- ksadk/server/static/assets/mscgen-qgSLujhx.js +1 -0
- ksadk/server/static/assets/mumps-CW_TBmxz.js +1 -0
- ksadk/server/static/assets/nginx-BMaDbqW3.js +1 -0
- ksadk/server/static/assets/nsis-AQ_alPln.js +1 -0
- ksadk/server/static/assets/ntriples-d28e9m0E.js +1 -0
- ksadk/server/static/assets/octave-D2Q8cUp1.js +1 -0
- ksadk/server/static/assets/ordinal-hYBb2elL.js +1 -0
- ksadk/server/static/assets/oz-BwTct_5T.js +1 -0
- ksadk/server/static/assets/packet-YPE3B663-BLKhbXZB.js +1 -0
- ksadk/server/static/assets/pascal-Dp_KdFgX.js +1 -0
- ksadk/server/static/assets/perl-CdEHsPfU.js +1 -0
- ksadk/server/static/assets/pie-LRSECV5Y-DFOikjSY.js +1 -0
- ksadk/server/static/assets/pieDiagram-4H26LBE5-DPnuCB8c.js +30 -0
- ksadk/server/static/assets/pig-AF4Hwhju.js +1 -0
- ksadk/server/static/assets/powershell-CzG71I4L.js +1 -0
- ksadk/server/static/assets/properties-C357ku5U.js +1 -0
- ksadk/server/static/assets/protobuf-D1ij_kNL.js +1 -0
- ksadk/server/static/assets/pug-Bugr-47h.js +1 -0
- ksadk/server/static/assets/puppet-B4hC2X4m.js +1 -0
- ksadk/server/static/assets/python-BqYw9LYJ.js +1 -0
- ksadk/server/static/assets/q-B9NhS7L_.js +1 -0
- ksadk/server/static/assets/quadrantDiagram-W4KKPZXB-miFpCPv4.js +7 -0
- ksadk/server/static/assets/r-DADKenSm.js +1 -0
- ksadk/server/static/assets/radar-GUYGQ44K-095b1Ikw.js +1 -0
- ksadk/server/static/assets/requirementDiagram-4Y6WPE33-mjB_yYkW.js +84 -0
- ksadk/server/static/assets/rpm-gpDK5sbt.js +1 -0
- ksadk/server/static/assets/ruby-DsYpTuWg.js +1 -0
- ksadk/server/static/assets/sankeyDiagram-5OEKKPKP-D7hSlZZZ.js +40 -0
- ksadk/server/static/assets/sas-C2OyD2Y6.js +1 -0
- ksadk/server/static/assets/scheme-CyDdOh7e.js +1 -0
- ksadk/server/static/assets/sequenceDiagram-3UESZ5HK-D9Cx6hC3.js +162 -0
- ksadk/server/static/assets/shell-Du5Qg9im.js +1 -0
- ksadk/server/static/assets/sieve-BAxa3IjF.js +1 -0
- ksadk/server/static/assets/simple-mode-DRpGK0lJ.js +1 -0
- ksadk/server/static/assets/smalltalk-BLp5-jwC.js +1 -0
- ksadk/server/static/assets/solr-BBopId9w.js +1 -0
- ksadk/server/static/assets/sparql-Dz-mlCAC.js +1 -0
- ksadk/server/static/assets/spreadsheet-CTIncYxj.js +1 -0
- ksadk/server/static/assets/sql-DcCJ9jop.js +1 -0
- ksadk/server/static/assets/stateDiagram-AJRCARHV-D_zrg47o.js +1 -0
- ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-J9kCb0nC.js +1 -0
- ksadk/server/static/assets/stex-sDrdk2BW.js +1 -0
- ksadk/server/static/assets/stylus-DLLZ8dgm.js +1 -0
- ksadk/server/static/assets/swift-DKB6_1j6.js +1 -0
- ksadk/server/static/assets/tcl-BGfruO1u.js +1 -0
- ksadk/server/static/assets/textile-DWBqthVk.js +1 -0
- ksadk/server/static/assets/tiddlywiki-VlUiK86J.js +1 -0
- ksadk/server/static/assets/tiki-CxVlf5KZ.js +1 -0
- ksadk/server/static/assets/timeline-definition-PNZ67QCA-CwSec0Wa.js +120 -0
- ksadk/server/static/assets/toml-LvoKBwCs.js +1 -0
- ksadk/server/static/assets/treeView-BLDUP644-yinfJEY6.js +1 -0
- ksadk/server/static/assets/treemap-LRROVOQU-DosUqN1Q.js +1 -0
- ksadk/server/static/assets/troff-C2dAgh7P.js +1 -0
- ksadk/server/static/assets/ttcn-BePYh-hG.js +1 -0
- ksadk/server/static/assets/ttcn-cfg-CY0aot7Q.js +1 -0
- ksadk/server/static/assets/turtle-BDLWE2cD.js +1 -0
- ksadk/server/static/assets/vb-BthdM_4N.js +1 -0
- ksadk/server/static/assets/vbscript-D9f6rSsU.js +1 -0
- ksadk/server/static/assets/velocity-CqftRsjg.js +1 -0
- ksadk/server/static/assets/vennDiagram-CIIHVFJN-agCVobiF.js +34 -0
- ksadk/server/static/assets/verilog-D-x6ne0Z.js +1 -0
- ksadk/server/static/assets/vhdl-CMrOq2q1.js +1 -0
- ksadk/server/static/assets/wardley-L42UT6IY-Bvp7-5uH.js +1 -0
- ksadk/server/static/assets/wardleyDiagram-YWT4CUSO-TmrlhWNt.js +78 -0
- ksadk/server/static/assets/webidl-D5jiJvOU.js +1 -0
- ksadk/server/static/assets/xquery-fj_R-kMw.js +1 -0
- ksadk/server/static/assets/xterm-DooSxjI5.js +36 -0
- ksadk/server/static/assets/xychartDiagram-2RQKCTM6-DV71V-ah.js +7 -0
- ksadk/server/static/assets/yacas-CXXgtw0p.js +1 -0
- ksadk/server/static/assets/z80-CL1naaMV.js +1 -0
- ksadk/server/static/favicon.svg +1 -0
- ksadk/server/static/icons.svg +24 -0
- ksadk/server/static/index.html +14 -0
- ksadk/server/terminal_sessions.py +551 -0
- ksadk/sessions/__init__.py +283 -0
- ksadk/sessions/base.py +354 -0
- ksadk/sessions/continuity.py +313 -0
- ksadk/sessions/errors.py +7 -0
- ksadk/sessions/in_memory.py +273 -0
- ksadk/sessions/local_service.py +949 -0
- ksadk/sessions/postgres_service.py +838 -0
- ksadk/sessions/sqlite_service.py +13 -0
- ksadk/skills/__init__.py +9 -0
- ksadk/skills/loader.py +46 -0
- ksadk/skills/models.py +120 -0
- ksadk/skills/package_store.py +104 -0
- ksadk/skills/runtime/__init__.py +17 -0
- ksadk/skills/runtime/agent.py +183 -0
- ksadk/skills/runtime/artifacts.py +39 -0
- ksadk/skills/runtime/backends/__init__.py +1 -0
- ksadk/skills/runtime/backends/disabled.py +8 -0
- ksadk/skills/runtime/backends/e2b.py +168 -0
- ksadk/skills/runtime/backends/local.py +95 -0
- ksadk/skills/runtime/base.py +103 -0
- ksadk/skills/runtime/executor.py +219 -0
- ksadk/skills/runtime/factory.py +37 -0
- ksadk/skills/runtime/loader.py +131 -0
- ksadk/skills/runtime/registry.py +150 -0
- ksadk/skills/runtime/request.py +67 -0
- ksadk/skills/service_client.py +239 -0
- ksadk/skills/service_env.py +58 -0
- ksadk/skills/tool_defs.py +221 -0
- ksadk/terminal_client.py +69 -0
- ksadk/terminal_exec_policy.py +229 -0
- ksadk/tools/__init__.py +17 -0
- ksadk/tools/gateway.py +230 -0
- ksadk/tools/result_budget.py +154 -0
- ksadk/toolsets/__init__.py +852 -0
- ksadk/toolsets/_langchain.py +11 -0
- ksadk/toolsets/platform.py +93 -0
- ksadk/toolsets/sandbox.py +310 -0
- ksadk/toolsets/skills.py +217 -0
- ksadk/toolsets/web.py +266 -0
- ksadk/toolsets/workspace.py +878 -0
- ksadk/toolsets/workspace_state.py +44 -0
- ksadk/tracing/__init__.py +37 -0
- ksadk/tracing/exporters/__init__.py +1 -0
- ksadk/tracing/exporters/inmemory_exporter.py +116 -0
- ksadk/tracing/exporters/langfuse_exporter.py +445 -0
- ksadk/tracing/setup.py +829 -0
- ksadk/tracing/span_utils.py +114 -0
- ksadk/tui/__init__.py +12 -0
- ksadk/tui/app.py +400 -0
- ksadk/tui/clipboard.py +100 -0
- ksadk/tui/widgets/__init__.py +22 -0
- ksadk/tui/widgets/assistant.py +89 -0
- ksadk/tui/widgets/base.py +54 -0
- ksadk/tui/widgets/chat_input.py +246 -0
- ksadk/tui/widgets/history.py +156 -0
- ksadk/tui/widgets/system.py +87 -0
- ksadk/tui/widgets/thinking.py +110 -0
- ksadk/tui/widgets/tool_call.py +161 -0
- ksadk/tui/widgets/user.py +37 -0
- ksadk/ui_config.py +168 -0
- ksadk/version.py +4 -0
- ksadk_runtime_common/__init__.py +20 -0
- ksadk_runtime_common/memory_backend/__init__.py +29 -0
- ksadk_runtime_common/memory_backend/manifest.py +79 -0
- ksadk_runtime_common/memory_backend/providers/__init__.py +6 -0
- ksadk_runtime_common/memory_backend/providers/lancedb.py +151 -0
- ksadk_runtime_common/memory_backend/providers/mem0.py +74 -0
- ksadk_runtime_common/memory_backend/registry.py +56 -0
- ksadk_runtime_common/memory_backend/render.py +71 -0
- ksadk_runtime_common/schemas/memory_backend_manifest.schema.json +148 -0
- ksadk_runtime_common/workspace_files/__init__.py +29 -0
- ksadk_runtime_common/workspace_files/bootstrap.py +56 -0
- ksadk_runtime_common/workspace_files/constants.py +9 -0
- ksadk_runtime_common/workspace_files/path_utils.py +71 -0
- ksadk_runtime_common/workspace_files/preview.py +96 -0
- ksadk_runtime_common/workspace_files/router.py +254 -0
- agentengine_sdk_python-0.6.2.dist-info/METADATA +0 -46
- agentengine_sdk_python-0.6.2.dist-info/RECORD +0 -4
- agentengine_sdk_python-0.6.2.dist-info/top_level.txt +0 -1
ksadk/cli/cmd_create.py
ADDED
|
@@ -0,0 +1,2277 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ksadk create - 创建项目模板
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import platform
|
|
7
|
+
import shlex
|
|
8
|
+
import click
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import shutil
|
|
11
|
+
import questionary
|
|
12
|
+
from ksadk.cli.cmd_config import custom_style
|
|
13
|
+
from ksadk.cli.ui import (
|
|
14
|
+
print_error,
|
|
15
|
+
print_info,
|
|
16
|
+
print_kv,
|
|
17
|
+
print_rule,
|
|
18
|
+
print_success,
|
|
19
|
+
print_title,
|
|
20
|
+
print_warn,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _quote_powershell_literal(value: str) -> str:
|
|
25
|
+
return "'" + str(value).replace("'", "''") + "'"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _quote_cmd_path(value: str) -> str:
|
|
29
|
+
return '"' + str(value).replace('"', '""') + '"'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _quick_start_command_lines(
|
|
33
|
+
project_name: str,
|
|
34
|
+
commands: list[str],
|
|
35
|
+
*,
|
|
36
|
+
system: str | None = None,
|
|
37
|
+
) -> list[str]:
|
|
38
|
+
system_name = system or platform.system()
|
|
39
|
+
normalized_commands = [str(command).strip() for command in commands if str(command).strip()]
|
|
40
|
+
if not normalized_commands:
|
|
41
|
+
return []
|
|
42
|
+
|
|
43
|
+
if system_name == "Windows":
|
|
44
|
+
cmd_chain = " && ".join(normalized_commands)
|
|
45
|
+
return [
|
|
46
|
+
"PowerShell:",
|
|
47
|
+
f"Set-Location -LiteralPath {_quote_powershell_literal(project_name)}",
|
|
48
|
+
*normalized_commands,
|
|
49
|
+
"cmd.exe:",
|
|
50
|
+
f"cd /d {_quote_cmd_path(project_name)} && {cmd_chain}",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
return [f"cd {shlex.quote(str(project_name))} && {' && '.join(normalized_commands)}"]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _print_quick_start_commands(project_name: str, commands: list[str]) -> None:
|
|
57
|
+
for line in _quick_start_command_lines(project_name, commands):
|
|
58
|
+
print_info(line)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
TEMPLATES = {
|
|
62
|
+
"adk": {
|
|
63
|
+
"agent.py": '''"""
|
|
64
|
+
{package_name} - ADK Agent
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
import os
|
|
68
|
+
from pathlib import Path
|
|
69
|
+
from dotenv import load_dotenv
|
|
70
|
+
|
|
71
|
+
load_dotenv(Path(__file__).parent.parent / ".env")
|
|
72
|
+
|
|
73
|
+
from google.adk.agents import Agent
|
|
74
|
+
from google.adk.models.lite_llm import LiteLlm
|
|
75
|
+
|
|
76
|
+
model = LiteLlm(
|
|
77
|
+
model=f"openai/{{os.getenv('OPENAI_MODEL_NAME', 'glm-5.2')}}",
|
|
78
|
+
api_base=os.getenv("OPENAI_BASE_URL"),
|
|
79
|
+
api_key=os.getenv("OPENAI_API_KEY"),
|
|
80
|
+
stream=True, # 启用流式输出
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def hello(name: str) -> dict:
|
|
85
|
+
"""问候工具
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
name: 名字
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
问候语
|
|
92
|
+
"""
|
|
93
|
+
return {{"message": f"你好, {{name}}!"}}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
root_agent = Agent(
|
|
97
|
+
name="{package_name}",
|
|
98
|
+
model=model,
|
|
99
|
+
description="ADK示例 Agent",
|
|
100
|
+
instruction="你是一个友好的助手。请用中文回复。",
|
|
101
|
+
tools=[hello],
|
|
102
|
+
)
|
|
103
|
+
''',
|
|
104
|
+
},
|
|
105
|
+
"langchain": {
|
|
106
|
+
"agent.py": '''"""
|
|
107
|
+
{package_name} - LangChain Agent
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
import os
|
|
111
|
+
from pathlib import Path
|
|
112
|
+
from dotenv import load_dotenv
|
|
113
|
+
|
|
114
|
+
load_dotenv(Path(__file__).parent.parent / ".env")
|
|
115
|
+
|
|
116
|
+
from langchain_openai import ChatOpenAI
|
|
117
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
118
|
+
from langchain_core.output_parsers import StrOutputParser
|
|
119
|
+
|
|
120
|
+
llm = ChatOpenAI(
|
|
121
|
+
model=os.getenv("OPENAI_MODEL_NAME", "glm-5.2"),
|
|
122
|
+
base_url=os.getenv("OPENAI_BASE_URL"),
|
|
123
|
+
api_key=os.getenv("OPENAI_API_KEY"),
|
|
124
|
+
streaming=True,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
prompt = ChatPromptTemplate.from_messages([
|
|
128
|
+
("system", "你是一个友好的助手。请用中文回复。"),
|
|
129
|
+
("human", "{{input}}")
|
|
130
|
+
])
|
|
131
|
+
|
|
132
|
+
root_agent = prompt | llm | StrOutputParser()
|
|
133
|
+
''',
|
|
134
|
+
},
|
|
135
|
+
"langgraph": {
|
|
136
|
+
"agent.py": '''"""
|
|
137
|
+
{package_name} - LangGraph Agent
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
import os
|
|
141
|
+
from pathlib import Path
|
|
142
|
+
from dotenv import load_dotenv
|
|
143
|
+
|
|
144
|
+
load_dotenv(Path(__file__).parent.parent / ".env")
|
|
145
|
+
|
|
146
|
+
from langchain_openai import ChatOpenAI
|
|
147
|
+
from langgraph.graph import StateGraph, END
|
|
148
|
+
from typing import TypedDict, Annotated
|
|
149
|
+
import operator
|
|
150
|
+
|
|
151
|
+
llm = ChatOpenAI(
|
|
152
|
+
model=os.getenv("OPENAI_MODEL_NAME", "glm-5.2"),
|
|
153
|
+
base_url=os.getenv("OPENAI_BASE_URL"),
|
|
154
|
+
api_key=os.getenv("OPENAI_API_KEY"),
|
|
155
|
+
streaming=True,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class State(TypedDict):
|
|
160
|
+
messages: Annotated[list, operator.add]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def chat(state: State):
|
|
164
|
+
messages = state["messages"]
|
|
165
|
+
response = llm.invoke(messages)
|
|
166
|
+
return {{"messages": [response]}}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
graph = StateGraph(State)
|
|
170
|
+
graph.add_node("chat", chat)
|
|
171
|
+
graph.set_entry_point("chat")
|
|
172
|
+
graph.add_edge("chat", END)
|
|
173
|
+
|
|
174
|
+
root_agent = graph.compile()
|
|
175
|
+
''',
|
|
176
|
+
},
|
|
177
|
+
"deepagents": {
|
|
178
|
+
"agent.py": '''"""
|
|
179
|
+
{package_name} - DeepAgents Agent
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
import os
|
|
183
|
+
from pathlib import Path
|
|
184
|
+
from dotenv import load_dotenv
|
|
185
|
+
|
|
186
|
+
load_dotenv(Path(__file__).parent.parent / ".env")
|
|
187
|
+
|
|
188
|
+
from deepagents import create_deep_agent
|
|
189
|
+
from langchain_openai import ChatOpenAI
|
|
190
|
+
|
|
191
|
+
llm = ChatOpenAI(
|
|
192
|
+
model=os.getenv("OPENAI_MODEL_NAME", "glm-5.2"),
|
|
193
|
+
base_url=os.getenv("OPENAI_BASE_URL"),
|
|
194
|
+
api_key=os.getenv("OPENAI_API_KEY"),
|
|
195
|
+
streaming=True,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
root_agent = create_deep_agent(model=llm)
|
|
199
|
+
''',
|
|
200
|
+
},
|
|
201
|
+
"openclaw": {
|
|
202
|
+
"agent.py": '''"""
|
|
203
|
+
{package_name} - OpenClaw placeholder
|
|
204
|
+
|
|
205
|
+
OpenClaw 使用预构建容器运行,当前目录主要用于保存 .env 和 .agentengine.state。
|
|
206
|
+
此入口仅用于兼容项目模板结构。
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
root_agent = None
|
|
210
|
+
''',
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _detect_framework(content: str) -> str:
|
|
216
|
+
"""检测 Agent 文件使用的框架"""
|
|
217
|
+
if 'from deepagents' in content or 'import deepagents' in content or 'create_deep_agent(' in content:
|
|
218
|
+
return 'deepagents'
|
|
219
|
+
elif 'from langgraph' in content or 'import langgraph' in content:
|
|
220
|
+
return 'langgraph'
|
|
221
|
+
elif 'from google.adk' in content or 'import google.adk' in content:
|
|
222
|
+
return 'adk'
|
|
223
|
+
elif 'from langchain' in content or 'import langchain' in content:
|
|
224
|
+
return 'langchain'
|
|
225
|
+
return 'unknown'
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _detect_agent_variable(content: str) -> str | None:
|
|
229
|
+
"""检测 Agent 变量名"""
|
|
230
|
+
import re
|
|
231
|
+
|
|
232
|
+
# 优先级匹配: root_agent > compiled graph > StateGraph > Agent()
|
|
233
|
+
# Only module-level exports are valid entry variables. Service-style
|
|
234
|
+
# projects often build a local `graph` inside init_agent_resources(), which
|
|
235
|
+
# must not be treated as importable module state.
|
|
236
|
+
patterns = [
|
|
237
|
+
(r'^(root_agent)\s*=', 'root_agent'),
|
|
238
|
+
(r'^(root_agent)\s*:\s*[^=]+\s*=', 'root_agent'), # type annotated root_agent
|
|
239
|
+
(r'^(\w+)\s*=\s*\w*graph\w*\.compile\(', None), # e.g., agent = graph.compile()
|
|
240
|
+
(r'^(\w+)\s*=\s*StateGraph', None), # e.g., graph = StateGraph(...)
|
|
241
|
+
(r'^(\w+)\s*=\s*Agent\(', None), # ADK: agent = Agent(...)
|
|
242
|
+
(r'^(\w+)\s*=\s*create_react_agent\(', None), # create_react_agent
|
|
243
|
+
(r'^(\w+)\s*=\s*create_deep_agent\(', None), # deepagents create_deep_agent
|
|
244
|
+
(r'^(\w+)\s*=\s*create_agent\(', None), # langchain create_agent
|
|
245
|
+
(r'^(\w+)\s*=\s*build_agent\(', None), # adapter style build_agent
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
for pattern, fixed_name in patterns:
|
|
249
|
+
match = re.search(pattern, content, re.MULTILINE | re.IGNORECASE)
|
|
250
|
+
if match:
|
|
251
|
+
return fixed_name if fixed_name else match.group(1)
|
|
252
|
+
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _has_agent_variable(content: str, agent_var: str) -> bool:
|
|
257
|
+
"""Best-effort static check that a module exposes the configured agent variable."""
|
|
258
|
+
import re
|
|
259
|
+
|
|
260
|
+
if not agent_var:
|
|
261
|
+
return False
|
|
262
|
+
escaped = re.escape(agent_var)
|
|
263
|
+
patterns = [
|
|
264
|
+
rf"^{escaped}\s*=",
|
|
265
|
+
rf"^{escaped}\s*:\s*[^=]+=",
|
|
266
|
+
rf"^from\s+[\.\w]+\s+import\s+.*\b{escaped}\b",
|
|
267
|
+
rf"^import\s+[\.\w]+\s+as\s+{escaped}\b",
|
|
268
|
+
]
|
|
269
|
+
return any(re.search(pattern, content, re.MULTILINE) for pattern in patterns)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _read_text_sig(path: Path) -> str:
|
|
273
|
+
return path.read_text(encoding="utf-8-sig")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _entry_exposes_variable(entry_path: Path, agent_var: str) -> bool:
|
|
277
|
+
try:
|
|
278
|
+
content = _read_text_sig(entry_path)
|
|
279
|
+
except Exception:
|
|
280
|
+
return False
|
|
281
|
+
return _has_agent_variable(content, agent_var) or _detect_agent_variable(content) == agent_var
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _load_entry_from_agentengine_yaml(directory: Path) -> tuple[Path, str] | None:
|
|
285
|
+
"""
|
|
286
|
+
从已有 agentengine.yaml 读取入口信息
|
|
287
|
+
返回 (入口文件路径, Agent 变量名) 或 None
|
|
288
|
+
"""
|
|
289
|
+
config_path = directory / "agentengine.yaml"
|
|
290
|
+
if not config_path.exists():
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
import yaml # type: ignore
|
|
295
|
+
|
|
296
|
+
config = yaml.safe_load(config_path.read_text(encoding="utf-8-sig")) or {}
|
|
297
|
+
except Exception:
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
entry_point = config.get("entry_point")
|
|
301
|
+
if not entry_point or not isinstance(entry_point, str):
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
# 兼容 Windows 路径分隔符
|
|
305
|
+
entry_path = directory / Path(entry_point.replace("\\", "/"))
|
|
306
|
+
if not entry_path.exists() or not entry_path.is_file():
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
agent_var = config.get("agent_variable")
|
|
310
|
+
if not isinstance(agent_var, str) or not agent_var.strip():
|
|
311
|
+
agent_var = "root_agent"
|
|
312
|
+
|
|
313
|
+
if not _entry_exposes_variable(entry_path, agent_var):
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
return entry_path, agent_var
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _load_entry_from_langgraph_json(directory: Path) -> tuple[Path, str] | None:
|
|
320
|
+
"""Read LangGraph's graph spec and return the first valid local graph target."""
|
|
321
|
+
config_path = directory / "langgraph.json"
|
|
322
|
+
if not config_path.exists():
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
config = json.loads(config_path.read_text(encoding="utf-8-sig"))
|
|
327
|
+
except Exception:
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
graphs = config.get("graphs")
|
|
331
|
+
if not isinstance(graphs, dict):
|
|
332
|
+
return None
|
|
333
|
+
|
|
334
|
+
for target in graphs.values():
|
|
335
|
+
if not isinstance(target, str) or ":" not in target:
|
|
336
|
+
continue
|
|
337
|
+
path_part, agent_var = target.rsplit(":", 1)
|
|
338
|
+
path_part = path_part.strip().removeprefix("./")
|
|
339
|
+
agent_var = agent_var.strip() or "root_agent"
|
|
340
|
+
entry_path = directory / Path(path_part.replace("\\", "/"))
|
|
341
|
+
if entry_path.exists() and entry_path.is_file() and _entry_exposes_variable(entry_path, agent_var):
|
|
342
|
+
return entry_path, agent_var
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _load_framework_from_agentengine_yaml(directory: Path) -> str | None:
|
|
347
|
+
"""从已有 agentengine.yaml 读取 framework"""
|
|
348
|
+
config_path = directory / "agentengine.yaml"
|
|
349
|
+
if not config_path.exists():
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
import yaml # type: ignore
|
|
354
|
+
|
|
355
|
+
config = yaml.safe_load(config_path.read_text(encoding="utf-8-sig")) or {}
|
|
356
|
+
except Exception:
|
|
357
|
+
return None
|
|
358
|
+
|
|
359
|
+
framework = config.get("framework")
|
|
360
|
+
if not isinstance(framework, str):
|
|
361
|
+
return None
|
|
362
|
+
framework = framework.strip().lower()
|
|
363
|
+
if framework in {"adk", "langchain", "langgraph", "deepagents", "openclaw", "hermes"}:
|
|
364
|
+
return framework
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _iter_python_files(directory: Path):
|
|
369
|
+
"""递归迭代目录下的 Python 文件(排除常见无关目录)"""
|
|
370
|
+
excluded_dirs = {
|
|
371
|
+
".git",
|
|
372
|
+
".venv",
|
|
373
|
+
"venv",
|
|
374
|
+
"__pycache__",
|
|
375
|
+
".pytest_cache",
|
|
376
|
+
".mypy_cache",
|
|
377
|
+
".ruff_cache",
|
|
378
|
+
".tox",
|
|
379
|
+
"node_modules",
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
for py_file in directory.rglob("*.py"):
|
|
383
|
+
rel_parts = py_file.relative_to(directory).parts
|
|
384
|
+
if any(part in excluded_dirs or part.startswith(".venv") for part in rel_parts):
|
|
385
|
+
continue
|
|
386
|
+
yield py_file
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _find_entry_file(directory: Path) -> tuple[Path, str] | None:
|
|
390
|
+
"""
|
|
391
|
+
在目录中查找入口文件
|
|
392
|
+
返回 (入口文件路径, Agent 变量名) 或 None
|
|
393
|
+
"""
|
|
394
|
+
# 1) 优先读取已有 agentengine.yaml(兼容已配置项目)
|
|
395
|
+
config_entry = _load_entry_from_agentengine_yaml(directory)
|
|
396
|
+
if config_entry:
|
|
397
|
+
entry_path, configured_var = config_entry
|
|
398
|
+
try:
|
|
399
|
+
content = entry_path.read_text(encoding="utf-8")
|
|
400
|
+
detected_var = _detect_agent_variable(content)
|
|
401
|
+
return (entry_path, detected_var or configured_var)
|
|
402
|
+
except Exception:
|
|
403
|
+
return (entry_path, configured_var)
|
|
404
|
+
|
|
405
|
+
# 2) LangGraph/DeepAgents Studio projects often declare the true graph in langgraph.json.
|
|
406
|
+
graph_entry = _load_entry_from_langgraph_json(directory)
|
|
407
|
+
if graph_entry:
|
|
408
|
+
return graph_entry
|
|
409
|
+
|
|
410
|
+
# 3) Service-style DeepAgents projects may only expose their graph through
|
|
411
|
+
# async init_agent_resources() and FastAPI lifespan. Return the init module
|
|
412
|
+
# so the wrapper can generate an AgentEngine adapter instead of pointing at
|
|
413
|
+
# a non-existent top-level graph variable.
|
|
414
|
+
service_entry = _find_deepagents_service_entry(directory)
|
|
415
|
+
if service_entry:
|
|
416
|
+
return service_entry
|
|
417
|
+
|
|
418
|
+
# 4) 按候选文件名递归查找(agent.py/main.py/app.py/...)
|
|
419
|
+
entry_candidates = ["agent.py", "main.py", "app.py", "agentengine_adapter.py", "__init__.py"]
|
|
420
|
+
for candidate in entry_candidates:
|
|
421
|
+
candidate_files = sorted(
|
|
422
|
+
(p for p in _iter_python_files(directory) if p.name == candidate),
|
|
423
|
+
key=lambda p: (len(p.relative_to(directory).parts), str(p)),
|
|
424
|
+
)
|
|
425
|
+
for entry_path in candidate_files:
|
|
426
|
+
try:
|
|
427
|
+
content = entry_path.read_text(encoding="utf-8")
|
|
428
|
+
except Exception:
|
|
429
|
+
continue
|
|
430
|
+
agent_var = _detect_agent_variable(content)
|
|
431
|
+
if agent_var:
|
|
432
|
+
return (entry_path, agent_var)
|
|
433
|
+
|
|
434
|
+
# 5) 全量扫描,按得分选择最可能入口
|
|
435
|
+
best_match: tuple[int, Path, str] | None = None
|
|
436
|
+
for py_file in _iter_python_files(directory):
|
|
437
|
+
if py_file.name.startswith("_") and py_file.name != "__init__.py":
|
|
438
|
+
continue
|
|
439
|
+
try:
|
|
440
|
+
content = py_file.read_text(encoding="utf-8")
|
|
441
|
+
except Exception:
|
|
442
|
+
continue
|
|
443
|
+
|
|
444
|
+
agent_var = _detect_agent_variable(content)
|
|
445
|
+
if not agent_var:
|
|
446
|
+
continue
|
|
447
|
+
|
|
448
|
+
rel = py_file.relative_to(directory)
|
|
449
|
+
depth = len(rel.parts)
|
|
450
|
+
score = 0
|
|
451
|
+
if agent_var == "root_agent":
|
|
452
|
+
score += 100
|
|
453
|
+
if py_file.name in entry_candidates:
|
|
454
|
+
score += 50
|
|
455
|
+
if "src" in rel.parts:
|
|
456
|
+
score += 20
|
|
457
|
+
score -= depth
|
|
458
|
+
|
|
459
|
+
if best_match is None or score > best_match[0]:
|
|
460
|
+
best_match = (score, py_file, agent_var)
|
|
461
|
+
|
|
462
|
+
if best_match:
|
|
463
|
+
return (best_match[1], best_match[2])
|
|
464
|
+
|
|
465
|
+
return None
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _fix_dotenv_paths_in_file(file_path: Path, depth: int = 1) -> bool:
|
|
469
|
+
"""
|
|
470
|
+
修复单个文件中的 load_dotenv 路径
|
|
471
|
+
depth: 文件相对于项目根目录的深度(用于确定需要多少个 .parent)
|
|
472
|
+
返回是否修改了文件
|
|
473
|
+
"""
|
|
474
|
+
import re
|
|
475
|
+
|
|
476
|
+
try:
|
|
477
|
+
content = file_path.read_text(encoding='utf-8')
|
|
478
|
+
except Exception:
|
|
479
|
+
return False
|
|
480
|
+
|
|
481
|
+
# 构建替换的 parent 链
|
|
482
|
+
parent_chain = '.parent' * depth
|
|
483
|
+
|
|
484
|
+
# 匹配各种 load_dotenv 模式
|
|
485
|
+
patterns = [
|
|
486
|
+
# load_dotenv(Path(__file__).parent / ".env")
|
|
487
|
+
(r'(load_dotenv\s*\(\s*Path\s*\(\s*__file__\s*\)\s*)\.parent(\s*/\s*["\']\.env["\'])',
|
|
488
|
+
rf'\1{parent_chain}\2'),
|
|
489
|
+
# load_dotenv(Path(__file__).parent.parent / ".env") -> 根据 depth 调整
|
|
490
|
+
]
|
|
491
|
+
|
|
492
|
+
modified = False
|
|
493
|
+
new_content = content
|
|
494
|
+
|
|
495
|
+
for pattern, replacement in patterns:
|
|
496
|
+
new_content_temp = re.sub(pattern, replacement, new_content)
|
|
497
|
+
if new_content_temp != new_content:
|
|
498
|
+
new_content = new_content_temp
|
|
499
|
+
modified = True
|
|
500
|
+
|
|
501
|
+
if modified:
|
|
502
|
+
file_path.write_text(new_content, encoding='utf-8')
|
|
503
|
+
|
|
504
|
+
return modified
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _fix_dotenv_paths_recursive(directory: Path, depth: int = 1) -> int:
|
|
508
|
+
"""
|
|
509
|
+
递归修复目录中所有 .py 文件的 load_dotenv 路径
|
|
510
|
+
返回修复的文件数量
|
|
511
|
+
"""
|
|
512
|
+
fixed_count = 0
|
|
513
|
+
|
|
514
|
+
for py_file in directory.rglob('*.py'):
|
|
515
|
+
# 计算相对深度
|
|
516
|
+
rel_path = py_file.relative_to(directory)
|
|
517
|
+
file_depth = len(rel_path.parts) + depth - 1 # 相对于项目根目录
|
|
518
|
+
|
|
519
|
+
if _fix_dotenv_paths_in_file(py_file, file_depth):
|
|
520
|
+
fixed_count += 1
|
|
521
|
+
|
|
522
|
+
return fixed_count
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _fix_nested_imports(file_path: Path, subdirs: list[str]) -> bool:
|
|
526
|
+
"""
|
|
527
|
+
修复文件中的嵌套目录导入
|
|
528
|
+
|
|
529
|
+
当源目录包含子包目录(如 my_agent/)时,
|
|
530
|
+
入口文件中的 `from my_agent import xxx` 需要改为 `from .my_agent import xxx`
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
file_path: 需要修复的文件
|
|
534
|
+
subdirs: 同级子目录名列表
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
是否修改了文件
|
|
538
|
+
"""
|
|
539
|
+
import re
|
|
540
|
+
|
|
541
|
+
if not subdirs:
|
|
542
|
+
return False
|
|
543
|
+
|
|
544
|
+
try:
|
|
545
|
+
content = file_path.read_text(encoding='utf-8')
|
|
546
|
+
except Exception:
|
|
547
|
+
return False
|
|
548
|
+
|
|
549
|
+
original = content
|
|
550
|
+
|
|
551
|
+
# 为每个子目录生成修复规则
|
|
552
|
+
for subdir in subdirs:
|
|
553
|
+
# from my_agent import xxx -> from .my_agent import xxx
|
|
554
|
+
pattern = rf'^(from\s+)({re.escape(subdir)})(\s+import\s+)'
|
|
555
|
+
replacement = rf'\1.\2\3'
|
|
556
|
+
content = re.sub(pattern, replacement, content, flags=re.MULTILINE)
|
|
557
|
+
|
|
558
|
+
# import my_agent -> from . import my_agent
|
|
559
|
+
# 只处理独立的 import 语句
|
|
560
|
+
pattern_import = rf'^import\s+({re.escape(subdir)})\s*$'
|
|
561
|
+
replacement_import = rf'from . import \1'
|
|
562
|
+
content = re.sub(pattern_import, replacement_import, content, flags=re.MULTILINE)
|
|
563
|
+
|
|
564
|
+
if content != original:
|
|
565
|
+
file_path.write_text(content, encoding='utf-8')
|
|
566
|
+
return True
|
|
567
|
+
|
|
568
|
+
return False
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _generate_env_content(global_env: dict) -> str:
|
|
572
|
+
"""生成 .env 文件内容"""
|
|
573
|
+
api_key = global_env.get("OPENAI_API_KEY", "")
|
|
574
|
+
base_url = global_env.get("OPENAI_BASE_URL", "")
|
|
575
|
+
model_name = global_env.get("OPENAI_MODEL_NAME", "")
|
|
576
|
+
ks_ak = global_env.get("KSYUN_ACCESS_KEY", "")
|
|
577
|
+
ks_sk = global_env.get("KSYUN_SECRET_KEY", "")
|
|
578
|
+
ks_region = global_env.get("KSYUN_REGION", "cn-beijing-6")
|
|
579
|
+
ks_account = global_env.get("KSYUN_ACCOUNT_ID", "")
|
|
580
|
+
|
|
581
|
+
env_content = f"""# 模型配置
|
|
582
|
+
OPENAI_API_KEY={api_key}
|
|
583
|
+
"""
|
|
584
|
+
if base_url:
|
|
585
|
+
env_content += f"OPENAI_BASE_URL={base_url}\n"
|
|
586
|
+
else:
|
|
587
|
+
env_content += "# OPENAI_BASE_URL=\n"
|
|
588
|
+
if model_name:
|
|
589
|
+
env_content += f"OPENAI_MODEL_NAME={model_name}\n"
|
|
590
|
+
else:
|
|
591
|
+
env_content += "# OPENAI_MODEL_NAME=\n"
|
|
592
|
+
|
|
593
|
+
env_content += f"""
|
|
594
|
+
# 金山云配置
|
|
595
|
+
"""
|
|
596
|
+
if ks_ak:
|
|
597
|
+
env_content += f"KSYUN_ACCESS_KEY={ks_ak}\n"
|
|
598
|
+
else:
|
|
599
|
+
env_content += "# KSYUN_ACCESS_KEY=\n"
|
|
600
|
+
if ks_sk:
|
|
601
|
+
env_content += f"KSYUN_SECRET_KEY={ks_sk}\n"
|
|
602
|
+
else:
|
|
603
|
+
env_content += "# KSYUN_SECRET_KEY=\n"
|
|
604
|
+
env_content += f"KSYUN_REGION={ks_region}\n"
|
|
605
|
+
if ks_account:
|
|
606
|
+
env_content += f"KSYUN_ACCOUNT_ID={ks_account}\n"
|
|
607
|
+
else:
|
|
608
|
+
env_content += "# KSYUN_ACCOUNT_ID=\n"
|
|
609
|
+
|
|
610
|
+
return env_content
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _generate_requirements_from_imports(directory: Path, framework: str) -> str:
|
|
614
|
+
"""
|
|
615
|
+
扫描目录中的 Python 文件,从 import 语句生成 requirements.txt
|
|
616
|
+
"""
|
|
617
|
+
import re
|
|
618
|
+
|
|
619
|
+
# 常用包名到 PyPI 包名的映射
|
|
620
|
+
import_to_package = {
|
|
621
|
+
'langchain': 'langchain',
|
|
622
|
+
'langchain_openai': 'langchain-openai',
|
|
623
|
+
'langchain_anthropic': 'langchain-anthropic',
|
|
624
|
+
'langchain_core': 'langchain-core',
|
|
625
|
+
'langchain_community': 'langchain-community',
|
|
626
|
+
'langgraph': 'langgraph',
|
|
627
|
+
'deepagents': 'deepagents',
|
|
628
|
+
'openai': 'openai',
|
|
629
|
+
'anthropic': 'anthropic',
|
|
630
|
+
'dotenv': 'python-dotenv',
|
|
631
|
+
'pydantic': 'pydantic',
|
|
632
|
+
'httpx': 'httpx',
|
|
633
|
+
'requests': 'requests',
|
|
634
|
+
'google.adk': 'google-adk',
|
|
635
|
+
'google.genai': 'google-genai',
|
|
636
|
+
'tiktoken': 'tiktoken',
|
|
637
|
+
'faiss': 'faiss-cpu',
|
|
638
|
+
'chromadb': 'chromadb',
|
|
639
|
+
'tavily': 'tavily-python',
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
found_packages = set()
|
|
643
|
+
|
|
644
|
+
# 扫描所有 .py 文件
|
|
645
|
+
for py_file in directory.rglob('*.py'):
|
|
646
|
+
try:
|
|
647
|
+
content = py_file.read_text(encoding='utf-8')
|
|
648
|
+
except Exception:
|
|
649
|
+
continue
|
|
650
|
+
|
|
651
|
+
# 匹配 import 语句
|
|
652
|
+
# from xxx import yyy
|
|
653
|
+
# import xxx
|
|
654
|
+
import_patterns = [
|
|
655
|
+
r'^from\s+([\w\.]+)',
|
|
656
|
+
r'^import\s+([\w\.]+)',
|
|
657
|
+
]
|
|
658
|
+
|
|
659
|
+
for pattern in import_patterns:
|
|
660
|
+
for match in re.finditer(pattern, content, re.MULTILINE):
|
|
661
|
+
module = match.group(1).split('.')[0] # 取顶级模块
|
|
662
|
+
full_module = match.group(1)
|
|
663
|
+
|
|
664
|
+
# 检查是否在映射中
|
|
665
|
+
if full_module in import_to_package:
|
|
666
|
+
found_packages.add(import_to_package[full_module])
|
|
667
|
+
elif module in import_to_package:
|
|
668
|
+
found_packages.add(import_to_package[module])
|
|
669
|
+
# 检查常见的下划线包名
|
|
670
|
+
elif module.replace('_', '-') in ['langchain-openai', 'langchain-anthropic',
|
|
671
|
+
'langchain-core', 'langchain-community']:
|
|
672
|
+
found_packages.add(module.replace('_', '-'))
|
|
673
|
+
|
|
674
|
+
# 确保框架相关的核心依赖在列表中
|
|
675
|
+
if framework == 'langgraph':
|
|
676
|
+
found_packages.add('langgraph')
|
|
677
|
+
found_packages.add('langchain')
|
|
678
|
+
found_packages.add('langchain-openai')
|
|
679
|
+
elif framework == 'deepagents':
|
|
680
|
+
found_packages.add('deepagents')
|
|
681
|
+
found_packages.add('langgraph')
|
|
682
|
+
found_packages.add('langchain')
|
|
683
|
+
found_packages.add('langchain-openai')
|
|
684
|
+
elif framework == 'langchain':
|
|
685
|
+
found_packages.add('langchain')
|
|
686
|
+
found_packages.add('langchain-openai')
|
|
687
|
+
elif framework == 'adk':
|
|
688
|
+
found_packages.add('google-adk')
|
|
689
|
+
|
|
690
|
+
# 总是添加 python-dotenv
|
|
691
|
+
found_packages.add('python-dotenv')
|
|
692
|
+
|
|
693
|
+
# 按字母顺序排序
|
|
694
|
+
sorted_packages = sorted(found_packages)
|
|
695
|
+
return '\n'.join(sorted_packages) + '\n'
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _analyze_langgraph_state(content: str) -> dict:
|
|
699
|
+
"""Best-effort LangGraph state shape detection for adapter scaffolding."""
|
|
700
|
+
import re
|
|
701
|
+
|
|
702
|
+
message_markers = (
|
|
703
|
+
"MessagesState",
|
|
704
|
+
"add_messages",
|
|
705
|
+
'state["messages"]',
|
|
706
|
+
"state['messages']",
|
|
707
|
+
'"messages":',
|
|
708
|
+
"'messages':",
|
|
709
|
+
"messages:",
|
|
710
|
+
)
|
|
711
|
+
if any(marker in content for marker in message_markers):
|
|
712
|
+
return {"kind": "messages", "input_field": None}
|
|
713
|
+
|
|
714
|
+
state_keys = set(re.findall(r"state\s*\[\s*[\"']([A-Za-z_][A-Za-z0-9_]*)[\"']\s*\]", content))
|
|
715
|
+
typed_fields = set(
|
|
716
|
+
re.findall(
|
|
717
|
+
r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(?:str|Optional\[str\]|list|dict|Any)",
|
|
718
|
+
content,
|
|
719
|
+
flags=re.MULTILINE,
|
|
720
|
+
)
|
|
721
|
+
)
|
|
722
|
+
candidates = [key for key in [*state_keys, *typed_fields] if key != "messages"]
|
|
723
|
+
preferred = ("query", "question", "prompt", "user_input", "input")
|
|
724
|
+
input_field = next((field for field in preferred if field in candidates), None)
|
|
725
|
+
|
|
726
|
+
if candidates and ("TypedDict" in content or "StateGraph(" in content or "langgraph" in content):
|
|
727
|
+
return {"kind": "custom", "input_field": input_field}
|
|
728
|
+
if "StateGraph(" in content or "from langgraph" in content or "import langgraph" in content:
|
|
729
|
+
return {"kind": "ambiguous", "input_field": input_field}
|
|
730
|
+
return {"kind": "unknown", "input_field": None}
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def _langgraph_analysis_for_path(path: Path) -> dict:
|
|
734
|
+
if path.is_dir():
|
|
735
|
+
chunks: list[str] = []
|
|
736
|
+
skip_dirs = {".git", "__pycache__", ".venv", "venv", "env", ".mypy_cache", ".pytest_cache"}
|
|
737
|
+
for py_file in sorted(path.rglob("*.py")):
|
|
738
|
+
if any(part in skip_dirs for part in py_file.parts):
|
|
739
|
+
continue
|
|
740
|
+
try:
|
|
741
|
+
chunks.append(py_file.read_text(encoding="utf-8"))
|
|
742
|
+
except Exception:
|
|
743
|
+
return {"kind": "ambiguous", "input_field": None}
|
|
744
|
+
if chunks:
|
|
745
|
+
return _analyze_langgraph_state("\n\n".join(chunks))
|
|
746
|
+
return {"kind": "ambiguous", "input_field": None}
|
|
747
|
+
|
|
748
|
+
try:
|
|
749
|
+
content = path.read_text(encoding="utf-8")
|
|
750
|
+
except Exception:
|
|
751
|
+
return {"kind": "ambiguous", "input_field": None}
|
|
752
|
+
return _analyze_langgraph_state(content)
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def _generate_langgraph_adapter_content(
|
|
756
|
+
*,
|
|
757
|
+
import_module: str,
|
|
758
|
+
agent_var: str,
|
|
759
|
+
analysis: dict,
|
|
760
|
+
) -> str:
|
|
761
|
+
input_field = analysis.get("input_field")
|
|
762
|
+
if input_field:
|
|
763
|
+
return_body = f''' return {{
|
|
764
|
+
"{input_field}": payload.get("input", ""),
|
|
765
|
+
}}'''
|
|
766
|
+
else:
|
|
767
|
+
return_body = ''' # TODO: Map AgentEngine's chat payload to your LangGraph State.
|
|
768
|
+
# Common examples:
|
|
769
|
+
# return {"query": payload.get("input", "")}
|
|
770
|
+
# return {"question": payload.get("input", ""), "context": []}
|
|
771
|
+
return dict(payload)'''
|
|
772
|
+
|
|
773
|
+
return f'''"""
|
|
774
|
+
AgentEngine adapter generated for a LangGraph project.
|
|
775
|
+
|
|
776
|
+
Review ksadk_prepare_state if your graph uses a custom State TypedDict.
|
|
777
|
+
"""
|
|
778
|
+
|
|
779
|
+
from {import_module} import {agent_var} as root_agent
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def ksadk_prepare_state(payload: dict, session_context: dict) -> dict:
|
|
783
|
+
"""Map AgentEngine chat input to the LangGraph State expected by root_agent."""
|
|
784
|
+
_ = session_context
|
|
785
|
+
{return_body}
|
|
786
|
+
'''
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _write_langgraph_adapter(
|
|
790
|
+
*,
|
|
791
|
+
package_dir: Path,
|
|
792
|
+
original_entry_relative: Path,
|
|
793
|
+
agent_var: str,
|
|
794
|
+
analysis: dict,
|
|
795
|
+
) -> Path | None:
|
|
796
|
+
if analysis.get("kind") not in {"custom", "ambiguous"}:
|
|
797
|
+
return None
|
|
798
|
+
|
|
799
|
+
adapter_name = "agentengine_adapter.py"
|
|
800
|
+
if original_entry_relative.as_posix() == adapter_name:
|
|
801
|
+
adapter_name = "ksadk_agentengine_adapter.py"
|
|
802
|
+
adapter_relative = Path(adapter_name)
|
|
803
|
+
original_module = "." + ".".join(original_entry_relative.with_suffix("").parts)
|
|
804
|
+
adapter_content = _generate_langgraph_adapter_content(
|
|
805
|
+
import_module=original_module,
|
|
806
|
+
agent_var=agent_var,
|
|
807
|
+
analysis=analysis,
|
|
808
|
+
)
|
|
809
|
+
(package_dir / adapter_relative).write_text(adapter_content, encoding="utf-8")
|
|
810
|
+
return adapter_relative
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def _find_python_file_containing(directory: Path, needle: str) -> Path | None:
|
|
814
|
+
skip_dirs = {".git", "__pycache__", ".venv", "venv", "env", ".mypy_cache", ".pytest_cache"}
|
|
815
|
+
for py_file in sorted(directory.rglob("*.py")):
|
|
816
|
+
if any(part in skip_dirs for part in py_file.relative_to(directory).parts):
|
|
817
|
+
continue
|
|
818
|
+
try:
|
|
819
|
+
if needle in py_file.read_text(encoding="utf-8-sig"):
|
|
820
|
+
return py_file
|
|
821
|
+
except Exception:
|
|
822
|
+
continue
|
|
823
|
+
return None
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def _collect_python_text(directory: Path) -> str:
|
|
827
|
+
try:
|
|
828
|
+
return "\n".join(
|
|
829
|
+
py_file.read_text(encoding="utf-8-sig")
|
|
830
|
+
for py_file in sorted(_iter_python_files(directory))
|
|
831
|
+
)
|
|
832
|
+
except Exception:
|
|
833
|
+
return ""
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def _find_deepagents_service_entry(directory: Path) -> tuple[Path, str] | None:
|
|
837
|
+
init_file = _find_python_file_containing(directory, "init_agent_resources")
|
|
838
|
+
if not init_file:
|
|
839
|
+
return None
|
|
840
|
+
|
|
841
|
+
project_text = _collect_python_text(directory)
|
|
842
|
+
service_markers = ("FastAPI(", "lifespan=", "add_routes(", "DeepAgentRunnable")
|
|
843
|
+
if "deepagents" not in project_text and "create_deep_agent(" not in project_text:
|
|
844
|
+
return None
|
|
845
|
+
if not any(marker in project_text for marker in service_markers):
|
|
846
|
+
return None
|
|
847
|
+
|
|
848
|
+
return init_file, "root_agent"
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _detect_deepagents_service_project(
|
|
852
|
+
*,
|
|
853
|
+
package_dir: Path,
|
|
854
|
+
entry_relative: Path,
|
|
855
|
+
agent_var: str,
|
|
856
|
+
) -> dict | None:
|
|
857
|
+
"""Detect service-style DeepAgents projects that initialize the graph in app lifespan."""
|
|
858
|
+
entry_path = package_dir / entry_relative
|
|
859
|
+
try:
|
|
860
|
+
entry_content = _read_text_sig(entry_path)
|
|
861
|
+
except Exception:
|
|
862
|
+
entry_content = ""
|
|
863
|
+
|
|
864
|
+
if _has_agent_variable(entry_content, agent_var):
|
|
865
|
+
return None
|
|
866
|
+
|
|
867
|
+
init_file = _find_python_file_containing(package_dir, "init_agent_resources")
|
|
868
|
+
if not init_file:
|
|
869
|
+
return None
|
|
870
|
+
|
|
871
|
+
project_text = _collect_python_text(package_dir) or entry_content
|
|
872
|
+
|
|
873
|
+
service_markers = ("FastAPI(", "lifespan=", "add_routes(", "DeepAgentRunnable")
|
|
874
|
+
if "deepagents" not in project_text and "create_deep_agent(" not in project_text:
|
|
875
|
+
return None
|
|
876
|
+
if not any(marker in project_text for marker in service_markers):
|
|
877
|
+
return None
|
|
878
|
+
|
|
879
|
+
runnable_file = _find_python_file_containing(package_dir, "class DeepAgentRunnable")
|
|
880
|
+
return {
|
|
881
|
+
"init_module": "." + ".".join(init_file.relative_to(package_dir).with_suffix("").parts),
|
|
882
|
+
"runnable_module": (
|
|
883
|
+
"." + ".".join(runnable_file.relative_to(package_dir).with_suffix("").parts)
|
|
884
|
+
if runnable_file
|
|
885
|
+
else None
|
|
886
|
+
),
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def _generate_deepagents_service_adapter_content(analysis: dict) -> str:
|
|
891
|
+
init_module = analysis["init_module"]
|
|
892
|
+
runnable_module = analysis.get("runnable_module")
|
|
893
|
+
runnable_module_constant = f'RUNNABLE_MODULE = "{runnable_module}"' if runnable_module else "RUNNABLE_MODULE = None"
|
|
894
|
+
runnable_build = " return None\n"
|
|
895
|
+
if runnable_module:
|
|
896
|
+
runnable_build = """ try:
|
|
897
|
+
runnable_module = importlib.import_module(RUNNABLE_MODULE, __package__)
|
|
898
|
+
deep_agent_runnable = getattr(runnable_module, "DeepAgentRunnable")
|
|
899
|
+
return deep_agent_runnable(agent, _NullLangfuseManager())
|
|
900
|
+
except Exception:
|
|
901
|
+
return None
|
|
902
|
+
"""
|
|
903
|
+
|
|
904
|
+
return f'''"""
|
|
905
|
+
AgentEngine adapter generated for a service-style DeepAgents project.
|
|
906
|
+
|
|
907
|
+
It preserves the user's async resource initialization and exposes a root_agent
|
|
908
|
+
object that ksadk can load through the DeepAgents/LangGraph runner.
|
|
909
|
+
"""
|
|
910
|
+
|
|
911
|
+
import asyncio
|
|
912
|
+
import importlib
|
|
913
|
+
from typing import Any
|
|
914
|
+
|
|
915
|
+
try:
|
|
916
|
+
from langchain_core.callbacks import BaseCallbackHandler
|
|
917
|
+
except Exception: # pragma: no cover - dependency may be absent during static checks
|
|
918
|
+
BaseCallbackHandler = object
|
|
919
|
+
|
|
920
|
+
INIT_MODULE = "{init_module}"
|
|
921
|
+
{runnable_module_constant}
|
|
922
|
+
|
|
923
|
+
class _NoopCallbackHandler(BaseCallbackHandler):
|
|
924
|
+
pass
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
class _NullLangfuseManager:
|
|
928
|
+
callback_handler = _NoopCallbackHandler()
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
class AgentEngineDeepAgentsServiceAdapter:
|
|
932
|
+
def __init__(self) -> None:
|
|
933
|
+
self._lock = asyncio.Lock()
|
|
934
|
+
self._initialized = False
|
|
935
|
+
self._agent = None
|
|
936
|
+
self._runnable = None
|
|
937
|
+
self._resources = ()
|
|
938
|
+
|
|
939
|
+
async def _ensure_initialized(self) -> None:
|
|
940
|
+
if self._initialized:
|
|
941
|
+
return
|
|
942
|
+
async with self._lock:
|
|
943
|
+
if self._initialized:
|
|
944
|
+
return
|
|
945
|
+
init_module = importlib.import_module(INIT_MODULE, __package__)
|
|
946
|
+
init_agent_resources = getattr(init_module, "init_agent_resources")
|
|
947
|
+
resources = await init_agent_resources()
|
|
948
|
+
if isinstance(resources, tuple):
|
|
949
|
+
self._agent = resources[0]
|
|
950
|
+
self._resources = resources[1:]
|
|
951
|
+
else:
|
|
952
|
+
self._agent = resources
|
|
953
|
+
self._resources = ()
|
|
954
|
+
self._runnable = self._build_runnable(self._agent)
|
|
955
|
+
self._initialized = True
|
|
956
|
+
|
|
957
|
+
def _build_runnable(self, agent: Any) -> Any:
|
|
958
|
+
{runnable_build}
|
|
959
|
+
|
|
960
|
+
@staticmethod
|
|
961
|
+
def _message_from_payload(payload: Any) -> str:
|
|
962
|
+
if isinstance(payload, dict):
|
|
963
|
+
if payload.get("message") is not None:
|
|
964
|
+
return str(payload.get("message") or "")
|
|
965
|
+
if payload.get("input") is not None:
|
|
966
|
+
return str(payload.get("input") or "")
|
|
967
|
+
messages = payload.get("messages")
|
|
968
|
+
if isinstance(messages, list) and messages:
|
|
969
|
+
last = messages[-1]
|
|
970
|
+
content = getattr(last, "content", None)
|
|
971
|
+
if content is None and isinstance(last, dict):
|
|
972
|
+
content = last.get("content")
|
|
973
|
+
return str(content or "")
|
|
974
|
+
return str(payload or "")
|
|
975
|
+
|
|
976
|
+
@staticmethod
|
|
977
|
+
def _session_id_from_payload(payload: Any, config: dict | None) -> str | None:
|
|
978
|
+
if isinstance(payload, dict):
|
|
979
|
+
value = payload.get("session_id") or payload.get("thread_id")
|
|
980
|
+
if value:
|
|
981
|
+
return str(value)
|
|
982
|
+
configurable = (config or {{}}).get("configurable", {{}})
|
|
983
|
+
value = configurable.get("thread_id") if isinstance(configurable, dict) else None
|
|
984
|
+
return str(value) if value else None
|
|
985
|
+
|
|
986
|
+
@staticmethod
|
|
987
|
+
def _normalize_result(result: Any) -> dict:
|
|
988
|
+
if isinstance(result, dict):
|
|
989
|
+
if "output" in result:
|
|
990
|
+
return result
|
|
991
|
+
if "response" in result:
|
|
992
|
+
return {{"output": result.get("response"), "raw": result}}
|
|
993
|
+
if "messages" in result and result["messages"]:
|
|
994
|
+
last = result["messages"][-1]
|
|
995
|
+
content = getattr(last, "content", None)
|
|
996
|
+
if content is None and isinstance(last, dict):
|
|
997
|
+
content = last.get("content")
|
|
998
|
+
return {{"output": content, "raw": result}}
|
|
999
|
+
return {{"output": str(result) if result is not None else "", "raw": result}}
|
|
1000
|
+
|
|
1001
|
+
async def ainvoke(self, payload: Any, config: dict | None = None, **kwargs: Any) -> dict:
|
|
1002
|
+
await self._ensure_initialized()
|
|
1003
|
+
message = self._message_from_payload(payload)
|
|
1004
|
+
session_id = self._session_id_from_payload(payload, config)
|
|
1005
|
+
service_payload = {{"message": message}}
|
|
1006
|
+
if session_id:
|
|
1007
|
+
service_payload["thread_id"] = session_id
|
|
1008
|
+
|
|
1009
|
+
if self._runnable is not None and hasattr(self._runnable, "_ainvoke"):
|
|
1010
|
+
result = await self._runnable._ainvoke(service_payload, config=config, **kwargs)
|
|
1011
|
+
return self._normalize_result(result)
|
|
1012
|
+
|
|
1013
|
+
if hasattr(self._agent, "ainvoke"):
|
|
1014
|
+
result = await self._agent.ainvoke({{"messages": payload.get("messages", [])}} if isinstance(payload, dict) and payload.get("messages") else payload, config=config, **kwargs)
|
|
1015
|
+
return self._normalize_result(result)
|
|
1016
|
+
result = self._agent.invoke(payload, config=config, **kwargs)
|
|
1017
|
+
return self._normalize_result(result)
|
|
1018
|
+
|
|
1019
|
+
def invoke(self, payload: Any, config: dict | None = None, **kwargs: Any) -> dict:
|
|
1020
|
+
try:
|
|
1021
|
+
loop = asyncio.get_running_loop()
|
|
1022
|
+
except RuntimeError:
|
|
1023
|
+
return asyncio.run(self.ainvoke(payload, config=config, **kwargs))
|
|
1024
|
+
raise RuntimeError("Synchronous invoke cannot run while an event loop is already active; use ainvoke instead")
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def ksadk_prepare_state(payload: dict, session_context: dict) -> dict:
|
|
1028
|
+
message = payload.get("input") or payload.get("message") or ""
|
|
1029
|
+
return {{
|
|
1030
|
+
"message": message,
|
|
1031
|
+
"session_id": session_context.get("session_id"),
|
|
1032
|
+
"history": session_context.get("history", []),
|
|
1033
|
+
}}
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
root_agent = AgentEngineDeepAgentsServiceAdapter()
|
|
1037
|
+
'''
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def _write_deepagents_service_adapter(
|
|
1041
|
+
*,
|
|
1042
|
+
package_dir: Path,
|
|
1043
|
+
original_entry_relative: Path,
|
|
1044
|
+
agent_var: str,
|
|
1045
|
+
) -> Path | None:
|
|
1046
|
+
analysis = _detect_deepagents_service_project(
|
|
1047
|
+
package_dir=package_dir,
|
|
1048
|
+
entry_relative=original_entry_relative,
|
|
1049
|
+
agent_var=agent_var,
|
|
1050
|
+
)
|
|
1051
|
+
if not analysis:
|
|
1052
|
+
return None
|
|
1053
|
+
|
|
1054
|
+
adapter_name = "agentengine_adapter.py"
|
|
1055
|
+
if original_entry_relative.as_posix() == adapter_name:
|
|
1056
|
+
adapter_name = "ksadk_agentengine_adapter.py"
|
|
1057
|
+
adapter_relative = Path(adapter_name)
|
|
1058
|
+
adapter_content = _generate_deepagents_service_adapter_content(analysis)
|
|
1059
|
+
(package_dir / adapter_relative).write_text(adapter_content, encoding="utf-8")
|
|
1060
|
+
return adapter_relative
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def _wrap_agent_file(from_agent_path: Path, project_name: str, framework: str, agent_var: str):
|
|
1064
|
+
"""包装单个 Agent 文件到新项目"""
|
|
1065
|
+
import re
|
|
1066
|
+
|
|
1067
|
+
project_path = Path(project_name)
|
|
1068
|
+
package_name = project_path.name.replace('-', '_')
|
|
1069
|
+
|
|
1070
|
+
if project_path.exists():
|
|
1071
|
+
print_error(f"目录 '{project_name}' 已存在")
|
|
1072
|
+
raise SystemExit(1)
|
|
1073
|
+
|
|
1074
|
+
print_title("包装 Agent 文件")
|
|
1075
|
+
print_kv("创建项目", project_name)
|
|
1076
|
+
print_kv("框架", framework)
|
|
1077
|
+
print_kv("包装文件", str(from_agent_path))
|
|
1078
|
+
print_kv("Agent 变量", agent_var)
|
|
1079
|
+
|
|
1080
|
+
# 创建目录
|
|
1081
|
+
(project_path / package_name).mkdir(parents=True)
|
|
1082
|
+
|
|
1083
|
+
# 复制源文件并修复 .env 路径
|
|
1084
|
+
source_filename = from_agent_path.name
|
|
1085
|
+
dest_path = project_path / package_name / source_filename
|
|
1086
|
+
|
|
1087
|
+
# 读取源文件内容
|
|
1088
|
+
source_content = from_agent_path.read_text(encoding='utf-8')
|
|
1089
|
+
|
|
1090
|
+
# 修复 load_dotenv 路径:parent -> parent.parent (因为文件被移到了子目录)
|
|
1091
|
+
fixed_content = re.sub(
|
|
1092
|
+
r'(load_dotenv\s*\(\s*Path\s*\(\s*__file__\s*\)\s*\.parent)(\s*/\s*["\']\.env["\'])',
|
|
1093
|
+
r'\1.parent\2',
|
|
1094
|
+
source_content
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
if fixed_content != source_content:
|
|
1098
|
+
print_info("已自动修复 .env 加载路径")
|
|
1099
|
+
|
|
1100
|
+
# 写入目标文件
|
|
1101
|
+
dest_path.write_text(fixed_content, encoding='utf-8')
|
|
1102
|
+
|
|
1103
|
+
# 检测全局配置
|
|
1104
|
+
from ksadk.configs.global_config import (
|
|
1105
|
+
global_config_exists,
|
|
1106
|
+
get_env_from_global_config,
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
global_env = {}
|
|
1110
|
+
if global_config_exists():
|
|
1111
|
+
global_env = get_env_from_global_config()
|
|
1112
|
+
if global_env:
|
|
1113
|
+
print_info("检测到全局配置,已自动填充凭证")
|
|
1114
|
+
|
|
1115
|
+
# 生成 .env
|
|
1116
|
+
(project_path / ".env").write_text(_generate_env_content(global_env), encoding="utf-8-sig")
|
|
1117
|
+
|
|
1118
|
+
# LangGraph custom-state 项目生成 adapter,避免直接猜业务 State 语义。
|
|
1119
|
+
entry_relative = Path(source_filename)
|
|
1120
|
+
export_agent_var = agent_var
|
|
1121
|
+
if framework == "langgraph":
|
|
1122
|
+
analysis = _analyze_langgraph_state(fixed_content)
|
|
1123
|
+
if analysis["kind"] == "messages":
|
|
1124
|
+
print_info("LangGraph state 检测: messages-compatible")
|
|
1125
|
+
elif analysis["kind"] == "custom":
|
|
1126
|
+
adapter_relative = _write_langgraph_adapter(
|
|
1127
|
+
package_dir=project_path / package_name,
|
|
1128
|
+
original_entry_relative=entry_relative,
|
|
1129
|
+
agent_var=agent_var,
|
|
1130
|
+
analysis=analysis,
|
|
1131
|
+
)
|
|
1132
|
+
if adapter_relative:
|
|
1133
|
+
entry_relative = adapter_relative
|
|
1134
|
+
export_agent_var = "root_agent"
|
|
1135
|
+
print_info("LangGraph state 检测: custom-state adapter generated")
|
|
1136
|
+
elif analysis["kind"] == "ambiguous":
|
|
1137
|
+
adapter_relative = _write_langgraph_adapter(
|
|
1138
|
+
package_dir=project_path / package_name,
|
|
1139
|
+
original_entry_relative=entry_relative,
|
|
1140
|
+
agent_var=agent_var,
|
|
1141
|
+
analysis=analysis,
|
|
1142
|
+
)
|
|
1143
|
+
if adapter_relative:
|
|
1144
|
+
entry_relative = adapter_relative
|
|
1145
|
+
export_agent_var = "root_agent"
|
|
1146
|
+
print_warn("LangGraph state 检测: ambiguous adapter generated, review required")
|
|
1147
|
+
|
|
1148
|
+
# 生成 agentengine.yaml
|
|
1149
|
+
entry_module = entry_relative.with_suffix("").name
|
|
1150
|
+
(project_path / "agentengine.yaml").write_text(f"""# AgentEngine 项目配置 (Wrapped)
|
|
1151
|
+
name: {package_name}
|
|
1152
|
+
version: "1.0.0"
|
|
1153
|
+
|
|
1154
|
+
framework: {framework}
|
|
1155
|
+
entry_point: {package_name}/{entry_relative}
|
|
1156
|
+
agent_variable: {export_agent_var}
|
|
1157
|
+
""", encoding="utf-8-sig")
|
|
1158
|
+
|
|
1159
|
+
# 生成 __init__.py
|
|
1160
|
+
(project_path / package_name / "__init__.py").write_text(f'''"""
|
|
1161
|
+
{project_name} - Wrapped Agent
|
|
1162
|
+
"""
|
|
1163
|
+
from .{entry_module} import {export_agent_var} as root_agent
|
|
1164
|
+
__all__ = ["root_agent"]
|
|
1165
|
+
''', encoding="utf-8-sig")
|
|
1166
|
+
|
|
1167
|
+
# 生成 requirements.txt
|
|
1168
|
+
reqs = ""
|
|
1169
|
+
if framework == "langchain":
|
|
1170
|
+
reqs = "langchain\nlangchain-openai\npython-dotenv\n"
|
|
1171
|
+
elif framework == "langgraph":
|
|
1172
|
+
reqs = "langchain\nlangchain-openai\nlanggraph\npython-dotenv\n"
|
|
1173
|
+
elif framework == "deepagents":
|
|
1174
|
+
reqs = "deepagents\nlangchain\nlangchain-openai\nlanggraph\npython-dotenv\n"
|
|
1175
|
+
elif framework == "adk":
|
|
1176
|
+
reqs = "google-adk\npython-dotenv\n"
|
|
1177
|
+
(project_path / "requirements.txt").write_text(reqs, encoding="utf-8")
|
|
1178
|
+
|
|
1179
|
+
# 生成 README.md
|
|
1180
|
+
(project_path / "README.md").write_text(f"""# {project_name}
|
|
1181
|
+
|
|
1182
|
+
Wrapped Agent project (from `{from_agent_path.name}`).
|
|
1183
|
+
|
|
1184
|
+
## Quick Start
|
|
1185
|
+
|
|
1186
|
+
```bash
|
|
1187
|
+
cd {project_name}
|
|
1188
|
+
agentengine run -i .
|
|
1189
|
+
```
|
|
1190
|
+
""", encoding="utf-8-sig")
|
|
1191
|
+
|
|
1192
|
+
print_success("包装完成")
|
|
1193
|
+
print_rule("快速开始")
|
|
1194
|
+
_print_quick_start_commands(project_name, ["agentengine run -i ."])
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
def _wrap_agent_directory(from_agent_dir: Path, project_name: str, framework: str,
|
|
1198
|
+
entry_file: Path, agent_var: str):
|
|
1199
|
+
"""包装 Agent 目录到新项目"""
|
|
1200
|
+
import shutil
|
|
1201
|
+
|
|
1202
|
+
project_path = Path(project_name)
|
|
1203
|
+
package_name = project_path.name.replace('-', '_')
|
|
1204
|
+
source_dir_name = from_agent_dir.name
|
|
1205
|
+
|
|
1206
|
+
if project_path.exists():
|
|
1207
|
+
print_error(f"目录 '{project_name}' 已存在")
|
|
1208
|
+
raise SystemExit(1)
|
|
1209
|
+
|
|
1210
|
+
print_title("包装 Agent 目录")
|
|
1211
|
+
print_kv("创建项目", project_name)
|
|
1212
|
+
print_kv("框架", framework)
|
|
1213
|
+
print_kv("包装目录", str(from_agent_dir))
|
|
1214
|
+
print_kv("入口文件", entry_file.name)
|
|
1215
|
+
print_kv("Agent 变量", agent_var)
|
|
1216
|
+
|
|
1217
|
+
# 创建项目目录
|
|
1218
|
+
project_path.mkdir(parents=True)
|
|
1219
|
+
|
|
1220
|
+
# 复制整个源目录到项目中(作为 package)
|
|
1221
|
+
dest_package_path = project_path / package_name
|
|
1222
|
+
ignore_names = {
|
|
1223
|
+
".git",
|
|
1224
|
+
".venv",
|
|
1225
|
+
"venv",
|
|
1226
|
+
"__pycache__",
|
|
1227
|
+
".pytest_cache",
|
|
1228
|
+
".mypy_cache",
|
|
1229
|
+
".ruff_cache",
|
|
1230
|
+
".tox",
|
|
1231
|
+
"node_modules",
|
|
1232
|
+
".idea",
|
|
1233
|
+
".vscode",
|
|
1234
|
+
".DS_Store",
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
def _ignore_copytree(_dir: str, names: list[str]):
|
|
1238
|
+
ignored = set()
|
|
1239
|
+
for name in names:
|
|
1240
|
+
if name in ignore_names or name.startswith(".venv"):
|
|
1241
|
+
ignored.add(name)
|
|
1242
|
+
continue
|
|
1243
|
+
if name.endswith((".pyc", ".pyo")):
|
|
1244
|
+
ignored.add(name)
|
|
1245
|
+
return ignored
|
|
1246
|
+
|
|
1247
|
+
shutil.copytree(from_agent_dir, dest_package_path, ignore=_ignore_copytree)
|
|
1248
|
+
|
|
1249
|
+
print_info(f"已复制 {sum(1 for _ in dest_package_path.rglob('*.py'))} 个 Python 文件")
|
|
1250
|
+
|
|
1251
|
+
# 递归修复 .env 路径
|
|
1252
|
+
fixed_count = _fix_dotenv_paths_recursive(dest_package_path, depth=2)
|
|
1253
|
+
if fixed_count > 0:
|
|
1254
|
+
print_info(f"已自动修复 {fixed_count} 个文件的 .env 加载路径")
|
|
1255
|
+
|
|
1256
|
+
# 修复嵌套目录导入路径
|
|
1257
|
+
# 查找源目录中的子目录(作为 Python 包)
|
|
1258
|
+
subdirs = [d.name for d in from_agent_dir.iterdir()
|
|
1259
|
+
if d.is_dir() and not d.name.startswith('.') and not d.name.startswith('_')
|
|
1260
|
+
and (d / '__init__.py').exists()]
|
|
1261
|
+
|
|
1262
|
+
if subdirs:
|
|
1263
|
+
# 修复入口文件中的导入
|
|
1264
|
+
dest_entry_file = dest_package_path / entry_file.relative_to(from_agent_dir)
|
|
1265
|
+
if _fix_nested_imports(dest_entry_file, subdirs):
|
|
1266
|
+
print_info(f"已修复嵌套目录导入: {', '.join(subdirs)}")
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
# 检测全局配置
|
|
1270
|
+
from ksadk.configs.global_config import (
|
|
1271
|
+
global_config_exists,
|
|
1272
|
+
get_env_from_global_config,
|
|
1273
|
+
)
|
|
1274
|
+
|
|
1275
|
+
global_env = {}
|
|
1276
|
+
if global_config_exists():
|
|
1277
|
+
global_env = get_env_from_global_config()
|
|
1278
|
+
if global_env:
|
|
1279
|
+
print_info("检测到全局配置,已自动填充凭证")
|
|
1280
|
+
|
|
1281
|
+
# 生成 .env
|
|
1282
|
+
(project_path / ".env").write_text(_generate_env_content(global_env), encoding="utf-8-sig")
|
|
1283
|
+
|
|
1284
|
+
# 生成 agentengine.yaml
|
|
1285
|
+
# entry_point 相对于项目根目录
|
|
1286
|
+
entry_relative = entry_file.relative_to(from_agent_dir)
|
|
1287
|
+
export_agent_var = agent_var
|
|
1288
|
+
if framework == "deepagents":
|
|
1289
|
+
adapter_relative = _write_deepagents_service_adapter(
|
|
1290
|
+
package_dir=dest_package_path,
|
|
1291
|
+
original_entry_relative=entry_relative,
|
|
1292
|
+
agent_var=agent_var,
|
|
1293
|
+
)
|
|
1294
|
+
if adapter_relative:
|
|
1295
|
+
entry_relative = adapter_relative
|
|
1296
|
+
export_agent_var = "root_agent"
|
|
1297
|
+
print_info("DeepAgents service adapter generated")
|
|
1298
|
+
|
|
1299
|
+
if framework == "langgraph":
|
|
1300
|
+
analysis = _langgraph_analysis_for_path(dest_package_path)
|
|
1301
|
+
if analysis["kind"] == "messages":
|
|
1302
|
+
print_info("LangGraph state 检测: messages-compatible")
|
|
1303
|
+
elif analysis["kind"] == "custom":
|
|
1304
|
+
adapter_relative = _write_langgraph_adapter(
|
|
1305
|
+
package_dir=dest_package_path,
|
|
1306
|
+
original_entry_relative=entry_relative,
|
|
1307
|
+
agent_var=agent_var,
|
|
1308
|
+
analysis=analysis,
|
|
1309
|
+
)
|
|
1310
|
+
if adapter_relative:
|
|
1311
|
+
entry_relative = adapter_relative
|
|
1312
|
+
export_agent_var = "root_agent"
|
|
1313
|
+
print_info("LangGraph state 检测: custom-state adapter generated")
|
|
1314
|
+
elif analysis["kind"] == "ambiguous":
|
|
1315
|
+
adapter_relative = _write_langgraph_adapter(
|
|
1316
|
+
package_dir=dest_package_path,
|
|
1317
|
+
original_entry_relative=entry_relative,
|
|
1318
|
+
agent_var=agent_var,
|
|
1319
|
+
analysis=analysis,
|
|
1320
|
+
)
|
|
1321
|
+
if adapter_relative:
|
|
1322
|
+
entry_relative = adapter_relative
|
|
1323
|
+
export_agent_var = "root_agent"
|
|
1324
|
+
print_warn("LangGraph state 检测: ambiguous adapter generated, review required")
|
|
1325
|
+
|
|
1326
|
+
(project_path / "agentengine.yaml").write_text(f"""# AgentEngine 项目配置 (Wrapped Directory)
|
|
1327
|
+
name: {package_name}
|
|
1328
|
+
version: "1.0.0"
|
|
1329
|
+
|
|
1330
|
+
framework: {framework}
|
|
1331
|
+
entry_point: {package_name}/{entry_relative}
|
|
1332
|
+
agent_variable: {export_agent_var}
|
|
1333
|
+
""", encoding="utf-8-sig")
|
|
1334
|
+
|
|
1335
|
+
# 确保 __init__.py 正确导出 root_agent
|
|
1336
|
+
init_file = dest_package_path / "__init__.py"
|
|
1337
|
+
entry_module = ".".join(entry_relative.with_suffix("").parts) # e.g. src.agentengine_adapter
|
|
1338
|
+
expected_export_line = f"from .{entry_module} import {export_agent_var} as root_agent"
|
|
1339
|
+
|
|
1340
|
+
# 检查现有 __init__.py 是否已导出 root_agent
|
|
1341
|
+
init_has_export = False
|
|
1342
|
+
if init_file.exists():
|
|
1343
|
+
init_content = init_file.read_text(encoding='utf-8')
|
|
1344
|
+
if expected_export_line in init_content:
|
|
1345
|
+
init_has_export = True
|
|
1346
|
+
|
|
1347
|
+
if not init_has_export:
|
|
1348
|
+
# 追加或创建导出语句
|
|
1349
|
+
export_code = f'''
|
|
1350
|
+
# AgentEngine 导出 (自动添加)
|
|
1351
|
+
{expected_export_line}
|
|
1352
|
+
__all__ = ["root_agent"]
|
|
1353
|
+
'''
|
|
1354
|
+
if init_file.exists():
|
|
1355
|
+
# 追加到现有文件
|
|
1356
|
+
with open(init_file, 'a', encoding='utf-8') as f:
|
|
1357
|
+
f.write(export_code)
|
|
1358
|
+
print_info("已修复 __init__.py 导出")
|
|
1359
|
+
else:
|
|
1360
|
+
init_file.write_text(f'''"""
|
|
1361
|
+
{project_name} - Wrapped Agent
|
|
1362
|
+
"""
|
|
1363
|
+
{expected_export_line}
|
|
1364
|
+
__all__ = ["root_agent"]
|
|
1365
|
+
''', encoding="utf-8-sig")
|
|
1366
|
+
|
|
1367
|
+
# 处理 requirements.txt
|
|
1368
|
+
source_requirements = from_agent_dir / "requirements.txt"
|
|
1369
|
+
dest_requirements = project_path / "requirements.txt"
|
|
1370
|
+
|
|
1371
|
+
if source_requirements.exists():
|
|
1372
|
+
# 如果源目录有 requirements.txt,复制并补充必要的依赖
|
|
1373
|
+
import shutil as shutil_req
|
|
1374
|
+
shutil_req.copy(source_requirements, dest_requirements)
|
|
1375
|
+
print_info("已复制 requirements.txt")
|
|
1376
|
+
|
|
1377
|
+
# 检查是否缺少必要依赖,追加
|
|
1378
|
+
existing_reqs = dest_requirements.read_text(encoding='utf-8').lower()
|
|
1379
|
+
missing = []
|
|
1380
|
+
if framework == "langgraph" and 'langgraph' not in existing_reqs:
|
|
1381
|
+
missing.append("langgraph")
|
|
1382
|
+
if framework == "deepagents":
|
|
1383
|
+
if 'deepagents' not in existing_reqs:
|
|
1384
|
+
missing.append("deepagents")
|
|
1385
|
+
if 'langgraph' not in existing_reqs:
|
|
1386
|
+
missing.append("langgraph")
|
|
1387
|
+
if 'python-dotenv' not in existing_reqs and 'dotenv' not in existing_reqs:
|
|
1388
|
+
missing.append("python-dotenv")
|
|
1389
|
+
|
|
1390
|
+
if missing:
|
|
1391
|
+
with open(dest_requirements, 'a', encoding='utf-8') as f:
|
|
1392
|
+
f.write("\n# Added by agentengine\n")
|
|
1393
|
+
for pkg in missing:
|
|
1394
|
+
f.write(f"{pkg}\n")
|
|
1395
|
+
print_info(f"已补充依赖: {', '.join(missing)}")
|
|
1396
|
+
else:
|
|
1397
|
+
# 自动根据 import 生成 requirements.txt
|
|
1398
|
+
reqs = _generate_requirements_from_imports(dest_package_path, framework)
|
|
1399
|
+
dest_requirements.write_text(reqs, encoding="utf-8")
|
|
1400
|
+
print_info(f"已自动生成 requirements.txt ({reqs.count(chr(10))} 个依赖)")
|
|
1401
|
+
|
|
1402
|
+
# 生成 README.md
|
|
1403
|
+
(project_path / "README.md").write_text(f"""# {project_name}
|
|
1404
|
+
|
|
1405
|
+
Wrapped Agent project (from `{from_agent_dir.name}/`).
|
|
1406
|
+
|
|
1407
|
+
## Quick Start
|
|
1408
|
+
|
|
1409
|
+
```bash
|
|
1410
|
+
cd {project_name}
|
|
1411
|
+
agentengine run -i .
|
|
1412
|
+
```
|
|
1413
|
+
""", encoding="utf-8-sig")
|
|
1414
|
+
|
|
1415
|
+
# === 运行时验证 ===
|
|
1416
|
+
print_rule("验证 Agent 加载")
|
|
1417
|
+
import subprocess
|
|
1418
|
+
import sys
|
|
1419
|
+
|
|
1420
|
+
verify_code = f'''
|
|
1421
|
+
import sys
|
|
1422
|
+
sys.path.insert(0, ".")
|
|
1423
|
+
try:
|
|
1424
|
+
from {package_name} import root_agent
|
|
1425
|
+
print("TYPE:" + type(root_agent).__name__)
|
|
1426
|
+
except Exception as e:
|
|
1427
|
+
print("ERROR:" + str(e))
|
|
1428
|
+
sys.exit(1)
|
|
1429
|
+
'''
|
|
1430
|
+
|
|
1431
|
+
result = subprocess.run(
|
|
1432
|
+
[sys.executable, "-c", verify_code],
|
|
1433
|
+
cwd=str(project_path),
|
|
1434
|
+
capture_output=True,
|
|
1435
|
+
text=True,
|
|
1436
|
+
timeout=30
|
|
1437
|
+
)
|
|
1438
|
+
|
|
1439
|
+
if result.returncode == 0:
|
|
1440
|
+
output = result.stdout.strip()
|
|
1441
|
+
if output.startswith("TYPE:"):
|
|
1442
|
+
agent_type = output.split("TYPE:")[1]
|
|
1443
|
+
print_success(f"Agent 加载成功! 类型: {agent_type}")
|
|
1444
|
+
else:
|
|
1445
|
+
print_success("Agent 加载成功!")
|
|
1446
|
+
else:
|
|
1447
|
+
error_msg = result.stderr or result.stdout
|
|
1448
|
+
print_warn(f"Agent 加载警告: {error_msg[:200]}")
|
|
1449
|
+
print_info("提示: 请检查依赖是否已安装,或代码是否有错误")
|
|
1450
|
+
|
|
1451
|
+
print_success("包装完成")
|
|
1452
|
+
print_rule("快速开始")
|
|
1453
|
+
_print_quick_start_commands(project_name, ["agentengine run -i ."])
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
def _write_hermes_project_template(project_path: Path, project_name: str, package_name: str) -> None:
|
|
1457
|
+
"""生成 Hermes container-first 项目骨架。"""
|
|
1458
|
+
repo_template_root = Path(__file__).resolve().parents[2] / "deploy" / "hermes"
|
|
1459
|
+
if repo_template_root.exists():
|
|
1460
|
+
replacements = {
|
|
1461
|
+
"{{PROJECT_NAME}}": project_name,
|
|
1462
|
+
"{{PACKAGE_NAME}}": package_name,
|
|
1463
|
+
}
|
|
1464
|
+
for source in repo_template_root.rglob("*"):
|
|
1465
|
+
relative = source.relative_to(repo_template_root)
|
|
1466
|
+
if source.is_dir():
|
|
1467
|
+
(project_path / relative).mkdir(parents=True, exist_ok=True)
|
|
1468
|
+
continue
|
|
1469
|
+
destination_relative = relative
|
|
1470
|
+
if source.name.endswith(".template"):
|
|
1471
|
+
destination_relative = relative.with_name(source.name[:-9])
|
|
1472
|
+
content = source.read_text(encoding="utf-8")
|
|
1473
|
+
for key, value in replacements.items():
|
|
1474
|
+
content = content.replace(key, value)
|
|
1475
|
+
destination = project_path / destination_relative
|
|
1476
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
1477
|
+
destination.write_text(content, encoding="utf-8-sig")
|
|
1478
|
+
continue
|
|
1479
|
+
destination = project_path / destination_relative
|
|
1480
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
1481
|
+
shutil.copy2(source, destination)
|
|
1482
|
+
entrypoint = project_path / "entrypoint.sh"
|
|
1483
|
+
if entrypoint.exists():
|
|
1484
|
+
entrypoint.chmod(0o755)
|
|
1485
|
+
return
|
|
1486
|
+
|
|
1487
|
+
runtime_dir = project_path / "runtime"
|
|
1488
|
+
runtime_dir.mkdir(parents=True, exist_ok=True)
|
|
1489
|
+
|
|
1490
|
+
(project_path / "agentengine.yaml").write_text(f"""# AgentEngine Hermes 项目配置
|
|
1491
|
+
name: {package_name}
|
|
1492
|
+
version: "1.0.0"
|
|
1493
|
+
|
|
1494
|
+
framework: hermes
|
|
1495
|
+
artifact_type: Container
|
|
1496
|
+
|
|
1497
|
+
ui_profile: hermes
|
|
1498
|
+
ui_path: /
|
|
1499
|
+
|
|
1500
|
+
deploy:
|
|
1501
|
+
resources:
|
|
1502
|
+
cpu: "2"
|
|
1503
|
+
memory: "4Gi"
|
|
1504
|
+
scaling:
|
|
1505
|
+
min_replicas: 1
|
|
1506
|
+
max_replicas: 1
|
|
1507
|
+
concurrency: 1000
|
|
1508
|
+
""", encoding="utf-8-sig")
|
|
1509
|
+
|
|
1510
|
+
(project_path / "Dockerfile").write_text("""FROM python:3.12-slim
|
|
1511
|
+
|
|
1512
|
+
ENV PYTHONUNBUFFERED=1 \\
|
|
1513
|
+
PYTHONDONTWRITEBYTECODE=1 \\
|
|
1514
|
+
PIP_NO_CACHE_DIR=1 \\
|
|
1515
|
+
PORT=8080 \\
|
|
1516
|
+
API_SERVER_HOST=127.0.0.1 \\
|
|
1517
|
+
API_SERVER_PORT=8642 \\
|
|
1518
|
+
HERMES_DASHBOARD_HOST=127.0.0.1 \\
|
|
1519
|
+
HERMES_DASHBOARD_PORT=9119
|
|
1520
|
+
|
|
1521
|
+
WORKDIR /app
|
|
1522
|
+
|
|
1523
|
+
RUN apt-get update \\
|
|
1524
|
+
&& apt-get install -y --no-install-recommends bash curl tini \\
|
|
1525
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
1526
|
+
|
|
1527
|
+
RUN pip install --no-cache-dir \\
|
|
1528
|
+
"fastapi>=0.100" \\
|
|
1529
|
+
"uvicorn[standard]>=0.23" \\
|
|
1530
|
+
"httpx>=0.24" \\
|
|
1531
|
+
"pyyaml>=6.0" \\
|
|
1532
|
+
"python-dotenv>=1.0" \\
|
|
1533
|
+
"hermes-agent==0.9.0"
|
|
1534
|
+
|
|
1535
|
+
COPY runtime ./runtime
|
|
1536
|
+
COPY entrypoint.sh ./entrypoint.sh
|
|
1537
|
+
RUN chmod +x /app/entrypoint.sh
|
|
1538
|
+
|
|
1539
|
+
EXPOSE 8080
|
|
1540
|
+
|
|
1541
|
+
ENTRYPOINT ["tini", "--"]
|
|
1542
|
+
CMD ["/app/entrypoint.sh"]
|
|
1543
|
+
""", encoding="utf-8")
|
|
1544
|
+
|
|
1545
|
+
(project_path / "entrypoint.sh").write_text("""#!/usr/bin/env bash
|
|
1546
|
+
set -euo pipefail
|
|
1547
|
+
|
|
1548
|
+
export HOME="${HOME:-/home/agent}"
|
|
1549
|
+
export PORT="${PORT:-8080}"
|
|
1550
|
+
export API_SERVER_HOST="${API_SERVER_HOST:-127.0.0.1}"
|
|
1551
|
+
export API_SERVER_PORT="${API_SERVER_PORT:-8642}"
|
|
1552
|
+
export HERMES_DASHBOARD_HOST="${HERMES_DASHBOARD_HOST:-127.0.0.1}"
|
|
1553
|
+
export HERMES_DASHBOARD_PORT="${HERMES_DASHBOARD_PORT:-9119}"
|
|
1554
|
+
export API_SERVER_ENABLED="${API_SERVER_ENABLED:-true}"
|
|
1555
|
+
|
|
1556
|
+
mkdir -p "${HOME}/.hermes"
|
|
1557
|
+
|
|
1558
|
+
cat > "${HOME}/.hermes/.env" <<EOF
|
|
1559
|
+
OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
|
1560
|
+
OPENAI_BASE_URL=${OPENAI_BASE_URL:-}
|
|
1561
|
+
OPENAI_MODEL_NAME=${OPENAI_MODEL_NAME:-}
|
|
1562
|
+
API_SERVER_ENABLED=${API_SERVER_ENABLED}
|
|
1563
|
+
API_SERVER_KEY=${API_SERVER_KEY:-}
|
|
1564
|
+
API_SERVER_HOST=${API_SERVER_HOST}
|
|
1565
|
+
API_SERVER_PORT=${API_SERVER_PORT}
|
|
1566
|
+
EOF
|
|
1567
|
+
|
|
1568
|
+
cat > "${HOME}/.hermes/config.yaml" <<EOF
|
|
1569
|
+
model:
|
|
1570
|
+
provider: custom
|
|
1571
|
+
default: "${OPENAI_MODEL_NAME:-}"
|
|
1572
|
+
base_url: "${OPENAI_BASE_URL:-}"
|
|
1573
|
+
api_server:
|
|
1574
|
+
enabled: true
|
|
1575
|
+
host: "${API_SERVER_HOST}"
|
|
1576
|
+
port: ${API_SERVER_PORT}
|
|
1577
|
+
EOF
|
|
1578
|
+
|
|
1579
|
+
hermes gateway run --replace &
|
|
1580
|
+
HERMES_API_PID=$!
|
|
1581
|
+
|
|
1582
|
+
hermes dashboard --host "${HERMES_DASHBOARD_HOST}" --port "${HERMES_DASHBOARD_PORT}" --no-open &
|
|
1583
|
+
HERMES_DASHBOARD_PID=$!
|
|
1584
|
+
|
|
1585
|
+
cleanup() {
|
|
1586
|
+
kill "${HERMES_API_PID}" "${HERMES_DASHBOARD_PID}" 2>/dev/null || true
|
|
1587
|
+
}
|
|
1588
|
+
trap cleanup EXIT
|
|
1589
|
+
|
|
1590
|
+
exec uvicorn runtime.app:app --host 0.0.0.0 --port "${PORT}"
|
|
1591
|
+
""", encoding="utf-8")
|
|
1592
|
+
|
|
1593
|
+
(runtime_dir / "__init__.py").write_text("", encoding="utf-8")
|
|
1594
|
+
(runtime_dir / "app.py").write_text("""from __future__ import annotations
|
|
1595
|
+
|
|
1596
|
+
import asyncio
|
|
1597
|
+
import contextlib
|
|
1598
|
+
import json
|
|
1599
|
+
import os
|
|
1600
|
+
import pty
|
|
1601
|
+
import select
|
|
1602
|
+
import signal
|
|
1603
|
+
import termios
|
|
1604
|
+
from typing import Iterable
|
|
1605
|
+
|
|
1606
|
+
import httpx
|
|
1607
|
+
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
|
|
1608
|
+
from fastapi.responses import Response
|
|
1609
|
+
from starlette.websockets import WebSocketState
|
|
1610
|
+
|
|
1611
|
+
|
|
1612
|
+
TERMINAL_SUBPROTOCOL = "ks-terminal.v1"
|
|
1613
|
+
SHELL_METACHARS = set("|&;<>()$`\\\\\\n\\r")
|
|
1614
|
+
SINGLE_READONLY = {"status", "doctor", "version", "insights"}
|
|
1615
|
+
NESTED_READONLY = {
|
|
1616
|
+
"sessions": {"list": (2, 2), "show": (3, 3), "export": (3, 3)},
|
|
1617
|
+
"config": {"show": (2, 2), "check": (2, 2), "path": (2, 2), "env-path": (2, 2)},
|
|
1618
|
+
"skills": {"list": (2, 2), "audit": (2, 2), "check": (2, 2)},
|
|
1619
|
+
"tools": {"list": (2, 2)},
|
|
1620
|
+
"cron": {"list": (2, 2), "status": (2, 2)},
|
|
1621
|
+
"gateway": {"status": (2, 2)},
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
app = FastAPI()
|
|
1625
|
+
|
|
1626
|
+
|
|
1627
|
+
def _api_base() -> str:
|
|
1628
|
+
return f"http://{os.getenv('API_SERVER_HOST', '127.0.0.1')}:{os.getenv('API_SERVER_PORT', '8642')}"
|
|
1629
|
+
|
|
1630
|
+
|
|
1631
|
+
def _dashboard_base() -> str:
|
|
1632
|
+
return f"http://{os.getenv('HERMES_DASHBOARD_HOST', '127.0.0.1')}:{os.getenv('HERMES_DASHBOARD_PORT', '9119')}"
|
|
1633
|
+
|
|
1634
|
+
|
|
1635
|
+
def _validate_exec_argv(argv: Iterable[str]) -> list[str]:
|
|
1636
|
+
normalized = [str(item).strip() for item in argv]
|
|
1637
|
+
if not normalized:
|
|
1638
|
+
raise ValueError("missing argv")
|
|
1639
|
+
for item in normalized:
|
|
1640
|
+
if not item or item.startswith("-") or any(char in SHELL_METACHARS for char in item):
|
|
1641
|
+
raise ValueError(f"unsafe argv: {item}")
|
|
1642
|
+
if normalized[0] in SINGLE_READONLY:
|
|
1643
|
+
if len(normalized) != 1:
|
|
1644
|
+
raise ValueError("unsupported argv")
|
|
1645
|
+
return normalized
|
|
1646
|
+
nested = NESTED_READONLY.get(normalized[0])
|
|
1647
|
+
if not nested or len(normalized) < 2:
|
|
1648
|
+
raise ValueError("unsupported argv")
|
|
1649
|
+
bounds = nested.get(normalized[1])
|
|
1650
|
+
if not bounds:
|
|
1651
|
+
raise ValueError("unsupported argv")
|
|
1652
|
+
if not (bounds[0] <= len(normalized) <= bounds[1]):
|
|
1653
|
+
raise ValueError("unsupported argv")
|
|
1654
|
+
return normalized
|
|
1655
|
+
|
|
1656
|
+
|
|
1657
|
+
async def _proxy_http(request: Request, base_url: str, path: str) -> Response:
|
|
1658
|
+
target = f"{base_url}/{path.lstrip('/')}"
|
|
1659
|
+
if request.url.query:
|
|
1660
|
+
target = f"{target}?{request.url.query}"
|
|
1661
|
+
headers = {
|
|
1662
|
+
key: value
|
|
1663
|
+
for key, value in request.headers.items()
|
|
1664
|
+
if key.lower() not in {"host", "content-length"}
|
|
1665
|
+
}
|
|
1666
|
+
async with httpx.AsyncClient(timeout=None) as client:
|
|
1667
|
+
upstream = await client.request(
|
|
1668
|
+
request.method,
|
|
1669
|
+
target,
|
|
1670
|
+
headers=headers,
|
|
1671
|
+
content=await request.body(),
|
|
1672
|
+
)
|
|
1673
|
+
response_headers = {
|
|
1674
|
+
key: value
|
|
1675
|
+
for key, value in upstream.headers.items()
|
|
1676
|
+
if key.lower() not in {"content-encoding", "transfer-encoding", "connection"}
|
|
1677
|
+
}
|
|
1678
|
+
return Response(upstream.content, status_code=upstream.status_code, headers=response_headers)
|
|
1679
|
+
|
|
1680
|
+
|
|
1681
|
+
@app.get("/health")
|
|
1682
|
+
async def health() -> dict:
|
|
1683
|
+
return {"ok": True}
|
|
1684
|
+
|
|
1685
|
+
|
|
1686
|
+
@app.api_route("/v1/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"])
|
|
1687
|
+
async def proxy_api(path: str, request: Request) -> Response:
|
|
1688
|
+
return await _proxy_http(request, _api_base(), f"v1/{path}")
|
|
1689
|
+
|
|
1690
|
+
|
|
1691
|
+
def _set_winsize(fd: int, rows: int, cols: int) -> None:
|
|
1692
|
+
termios.tcsetwinsize(fd, (int(rows or 24), int(cols or 80)))
|
|
1693
|
+
|
|
1694
|
+
|
|
1695
|
+
async def _pty_reader(ws: WebSocket, fd: int) -> None:
|
|
1696
|
+
loop = asyncio.get_running_loop()
|
|
1697
|
+
while True:
|
|
1698
|
+
await loop.run_in_executor(None, lambda: select.select([fd], [], [], None))
|
|
1699
|
+
data = os.read(fd, 4096)
|
|
1700
|
+
if not data:
|
|
1701
|
+
return
|
|
1702
|
+
await ws.send_bytes(data)
|
|
1703
|
+
|
|
1704
|
+
|
|
1705
|
+
async def _wait_process(pid: int) -> int:
|
|
1706
|
+
loop = asyncio.get_running_loop()
|
|
1707
|
+
_, status = await loop.run_in_executor(None, os.waitpid, pid, 0)
|
|
1708
|
+
if os.WIFEXITED(status):
|
|
1709
|
+
return os.WEXITSTATUS(status)
|
|
1710
|
+
if os.WIFSIGNALED(status):
|
|
1711
|
+
return 128 + os.WTERMSIG(status)
|
|
1712
|
+
return status
|
|
1713
|
+
|
|
1714
|
+
|
|
1715
|
+
@app.websocket("/_ksadk/terminal/ws")
|
|
1716
|
+
async def terminal_ws(ws: WebSocket) -> None:
|
|
1717
|
+
if TERMINAL_SUBPROTOCOL not in (ws.headers.get("sec-websocket-protocol") or ""):
|
|
1718
|
+
await ws.close(code=4400, reason="missing ks-terminal.v1 subprotocol")
|
|
1719
|
+
return
|
|
1720
|
+
await ws.accept(subprotocol=TERMINAL_SUBPROTOCOL)
|
|
1721
|
+
pid = None
|
|
1722
|
+
fd = None
|
|
1723
|
+
receive_task = None
|
|
1724
|
+
reader_task = None
|
|
1725
|
+
wait_task = None
|
|
1726
|
+
try:
|
|
1727
|
+
first = await ws.receive_text()
|
|
1728
|
+
payload = json.loads(first)
|
|
1729
|
+
if payload.get("type") != "start":
|
|
1730
|
+
raise ValueError("first frame must be start")
|
|
1731
|
+
mode = payload.get("mode")
|
|
1732
|
+
argv = payload.get("argv") or []
|
|
1733
|
+
if mode == "tui":
|
|
1734
|
+
command = ["hermes", "chat"]
|
|
1735
|
+
elif mode == "exec":
|
|
1736
|
+
command = ["hermes", *_validate_exec_argv(argv)]
|
|
1737
|
+
else:
|
|
1738
|
+
raise ValueError("unsupported mode")
|
|
1739
|
+
|
|
1740
|
+
pid, fd = pty.fork()
|
|
1741
|
+
if pid == 0:
|
|
1742
|
+
os.execvp(command[0], command)
|
|
1743
|
+
|
|
1744
|
+
_set_winsize(fd, int(payload.get("rows") or 24), int(payload.get("cols") or 80))
|
|
1745
|
+
await ws.send_text(json.dumps({"type": "ready"}))
|
|
1746
|
+
reader_task = asyncio.create_task(_pty_reader(ws, fd))
|
|
1747
|
+
wait_task = asyncio.create_task(_wait_process(pid))
|
|
1748
|
+
receive_task = asyncio.create_task(ws.receive())
|
|
1749
|
+
|
|
1750
|
+
while True:
|
|
1751
|
+
done, _pending = await asyncio.wait({wait_task, receive_task}, return_when=asyncio.FIRST_COMPLETED)
|
|
1752
|
+
if wait_task in done:
|
|
1753
|
+
if reader_task:
|
|
1754
|
+
reader_task.cancel()
|
|
1755
|
+
if receive_task:
|
|
1756
|
+
receive_task.cancel()
|
|
1757
|
+
code = wait_task.result()
|
|
1758
|
+
if ws.client_state == WebSocketState.CONNECTED:
|
|
1759
|
+
await ws.send_text(json.dumps({"type": "exit", "code": code}))
|
|
1760
|
+
return
|
|
1761
|
+
|
|
1762
|
+
message = receive_task.result()
|
|
1763
|
+
receive_task = asyncio.create_task(ws.receive())
|
|
1764
|
+
if message.get("bytes") is not None:
|
|
1765
|
+
os.write(fd, message["bytes"])
|
|
1766
|
+
continue
|
|
1767
|
+
text = message.get("text")
|
|
1768
|
+
if not text:
|
|
1769
|
+
continue
|
|
1770
|
+
control = json.loads(text)
|
|
1771
|
+
if control.get("type") == "resize":
|
|
1772
|
+
_set_winsize(fd, int(control.get("rows") or 24), int(control.get("cols") or 80))
|
|
1773
|
+
elif control.get("type") == "signal":
|
|
1774
|
+
sig = signal.SIGINT if control.get("signal") == "SIGINT" else signal.SIGTERM
|
|
1775
|
+
os.kill(pid, sig)
|
|
1776
|
+
elif control.get("type") == "stdin_eof":
|
|
1777
|
+
os.close(fd)
|
|
1778
|
+
fd = None
|
|
1779
|
+
except WebSocketDisconnect:
|
|
1780
|
+
return
|
|
1781
|
+
except Exception as exc:
|
|
1782
|
+
if ws.client_state == WebSocketState.CONNECTED:
|
|
1783
|
+
await ws.send_text(json.dumps({"type": "error", "message": str(exc)}))
|
|
1784
|
+
await ws.close()
|
|
1785
|
+
finally:
|
|
1786
|
+
for task in (receive_task, reader_task):
|
|
1787
|
+
if task and not task.done():
|
|
1788
|
+
task.cancel()
|
|
1789
|
+
if pid is not None and (wait_task is None or not wait_task.done()):
|
|
1790
|
+
with contextlib.suppress(ProcessLookupError):
|
|
1791
|
+
os.kill(pid, signal.SIGTERM)
|
|
1792
|
+
if wait_task and not wait_task.done():
|
|
1793
|
+
wait_task.cancel()
|
|
1794
|
+
if fd is not None:
|
|
1795
|
+
with contextlib.suppress(OSError):
|
|
1796
|
+
os.close(fd)
|
|
1797
|
+
|
|
1798
|
+
|
|
1799
|
+
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"])
|
|
1800
|
+
async def proxy_dashboard(path: str, request: Request) -> Response:
|
|
1801
|
+
return await _proxy_http(request, _dashboard_base(), path)
|
|
1802
|
+
""", encoding="utf-8")
|
|
1803
|
+
|
|
1804
|
+
(project_path / "README.md").write_text(f"""# {project_name}
|
|
1805
|
+
|
|
1806
|
+
Hermes AgentEngine container-first 项目。
|
|
1807
|
+
|
|
1808
|
+
## 快速开始
|
|
1809
|
+
|
|
1810
|
+
```bash
|
|
1811
|
+
cd {project_name}
|
|
1812
|
+
|
|
1813
|
+
# 1. 编辑 .env,填写模型配置
|
|
1814
|
+
vim .env
|
|
1815
|
+
|
|
1816
|
+
# 2. 部署平台预置 Hermes runtime 镜像
|
|
1817
|
+
agentengine hermes deploy
|
|
1818
|
+
|
|
1819
|
+
# 3. 打开 Hermes 管理 UI
|
|
1820
|
+
agentengine hermes open
|
|
1821
|
+
|
|
1822
|
+
# 4. 进入 pod 内 Hermes 原生 TUI
|
|
1823
|
+
agentengine invoke
|
|
1824
|
+
|
|
1825
|
+
# 5. 使用统一 hosted chat
|
|
1826
|
+
agentengine hermes open --chat
|
|
1827
|
+
|
|
1828
|
+
# 6. 受限只读运维子命令
|
|
1829
|
+
agentengine hermes exec <agent> -- status
|
|
1830
|
+
|
|
1831
|
+
# 7. Pairing 审批透传
|
|
1832
|
+
agentengine hermes pairing <agent> -- list
|
|
1833
|
+
```
|
|
1834
|
+
|
|
1835
|
+
## Runtime Contract
|
|
1836
|
+
|
|
1837
|
+
- `/` 反代到 Hermes dashboard 管理 UI
|
|
1838
|
+
- `/chat` 由 AgentEngine hosted UI/router 处理
|
|
1839
|
+
- `/v1/*` 反代到 Hermes OpenAI-compatible API server
|
|
1840
|
+
- `/_ksadk/terminal/ws` 提供 `ks-terminal.v1` 原生 TUI/exec/pairing websocket
|
|
1841
|
+
""", encoding="utf-8-sig")
|
|
1842
|
+
|
|
1843
|
+
|
|
1844
|
+
@click.command(context_settings=dict(help_option_names=['-h', '--help']))
|
|
1845
|
+
@click.argument('project_name', required=False)
|
|
1846
|
+
@click.option('--framework', '-f', type=click.Choice(['adk', 'langchain', 'langgraph', 'deepagents', 'openclaw', 'hermes']),
|
|
1847
|
+
default='langgraph', help='框架类型 (default: langgraph)')
|
|
1848
|
+
@click.option('--from-agent', 'from_agent_path', type=click.Path(exists=True),
|
|
1849
|
+
help='包装现有 Agent 文件或目录')
|
|
1850
|
+
def create(project_name: str, framework: str, from_agent_path: str):
|
|
1851
|
+
"""创建新的 Agent 项目
|
|
1852
|
+
|
|
1853
|
+
PROJECT_NAME: 项目名称
|
|
1854
|
+
|
|
1855
|
+
使用 --from-agent 可包装现有代码:
|
|
1856
|
+
agentengine init --from-agent ./my_agent.py # 单文件
|
|
1857
|
+
agentengine init --from-agent ./my_agent/ # 目录
|
|
1858
|
+
"""
|
|
1859
|
+
# === 包装模式 ===
|
|
1860
|
+
if from_agent_path:
|
|
1861
|
+
from_path = Path(from_agent_path)
|
|
1862
|
+
|
|
1863
|
+
# === 目录模式 ===
|
|
1864
|
+
if from_path.is_dir():
|
|
1865
|
+
print_info(f"扫描目录: {from_path}")
|
|
1866
|
+
|
|
1867
|
+
# 查找入口文件
|
|
1868
|
+
entry_result = _find_entry_file(from_path)
|
|
1869
|
+
if not entry_result:
|
|
1870
|
+
print_error("未找到有效的入口文件 (agent.py, main.py, __init__.py 或包含 Agent 定义的文件)")
|
|
1871
|
+
raise SystemExit(1)
|
|
1872
|
+
|
|
1873
|
+
entry_file, detected_var = entry_result
|
|
1874
|
+
print_info(f"检测到入口: {entry_file.name}")
|
|
1875
|
+
print_info(f"检测到变量: {detected_var}")
|
|
1876
|
+
|
|
1877
|
+
# 检测框架:优先读取已有 agentengine.yaml
|
|
1878
|
+
detected_framework = _load_framework_from_agentengine_yaml(from_path) or 'unknown'
|
|
1879
|
+
if detected_framework != 'unknown':
|
|
1880
|
+
print_info(f"从 agentengine.yaml 检测到框架: {detected_framework}")
|
|
1881
|
+
else:
|
|
1882
|
+
entry_content = entry_file.read_text(encoding='utf-8')
|
|
1883
|
+
detected_framework = _detect_framework(entry_content)
|
|
1884
|
+
|
|
1885
|
+
# 如果入口文件未检测到框架,扫描整个目录
|
|
1886
|
+
if detected_framework == 'unknown':
|
|
1887
|
+
for py_file in _iter_python_files(from_path):
|
|
1888
|
+
try:
|
|
1889
|
+
content = py_file.read_text(encoding='utf-8')
|
|
1890
|
+
except Exception:
|
|
1891
|
+
continue
|
|
1892
|
+
detected_framework = _detect_framework(content)
|
|
1893
|
+
if detected_framework != 'unknown':
|
|
1894
|
+
break
|
|
1895
|
+
|
|
1896
|
+
if detected_framework == 'unknown':
|
|
1897
|
+
print_warn("无法自动检测框架,将使用默认 langgraph")
|
|
1898
|
+
detected_framework = 'langgraph'
|
|
1899
|
+
else:
|
|
1900
|
+
print_info(f"检测到框架: {detected_framework}")
|
|
1901
|
+
|
|
1902
|
+
# 如果没有指定项目名,使用目录名
|
|
1903
|
+
if not project_name:
|
|
1904
|
+
project_name = from_path.name.replace('_', '-')
|
|
1905
|
+
|
|
1906
|
+
_wrap_agent_directory(from_path, project_name, detected_framework, entry_file, detected_var)
|
|
1907
|
+
return
|
|
1908
|
+
|
|
1909
|
+
# === 单文件模式 ===
|
|
1910
|
+
else:
|
|
1911
|
+
content = from_path.read_text(encoding='utf-8')
|
|
1912
|
+
|
|
1913
|
+
# 自动检测框架
|
|
1914
|
+
detected_framework = _detect_framework(content)
|
|
1915
|
+
if detected_framework == 'unknown':
|
|
1916
|
+
print_warn("无法自动检测框架,将使用默认 langgraph")
|
|
1917
|
+
detected_framework = 'langgraph'
|
|
1918
|
+
else:
|
|
1919
|
+
print_info(f"检测到框架: {detected_framework}")
|
|
1920
|
+
|
|
1921
|
+
# 自动检测 Agent 变量
|
|
1922
|
+
detected_var = _detect_agent_variable(content)
|
|
1923
|
+
if not detected_var:
|
|
1924
|
+
print_warn("无法自动检测 Agent 变量,将使用默认 root_agent")
|
|
1925
|
+
detected_var = 'root_agent'
|
|
1926
|
+
else:
|
|
1927
|
+
print_info(f"检测到变量: {detected_var}")
|
|
1928
|
+
|
|
1929
|
+
# 如果没有指定项目名,使用文件名
|
|
1930
|
+
if not project_name:
|
|
1931
|
+
project_name = from_path.stem.replace('_', '-')
|
|
1932
|
+
|
|
1933
|
+
_wrap_agent_file(from_path, project_name, detected_framework, detected_var)
|
|
1934
|
+
return
|
|
1935
|
+
|
|
1936
|
+
# === 正常模板模式 ===
|
|
1937
|
+
# 如果没有提供项目名称,进入交互模式
|
|
1938
|
+
if not project_name:
|
|
1939
|
+
print_title("初始化新项目")
|
|
1940
|
+
|
|
1941
|
+
project_name = questionary.text(
|
|
1942
|
+
"请输入项目名称:",
|
|
1943
|
+
style=custom_style
|
|
1944
|
+
).ask()
|
|
1945
|
+
|
|
1946
|
+
if not project_name:
|
|
1947
|
+
print_error("取消创建")
|
|
1948
|
+
raise SystemExit(0)
|
|
1949
|
+
|
|
1950
|
+
framework = questionary.select(
|
|
1951
|
+
"请选择开发框架:",
|
|
1952
|
+
choices=['langgraph', 'langchain', 'deepagents', 'adk', 'openclaw', 'hermes'],
|
|
1953
|
+
default='langgraph',
|
|
1954
|
+
style=custom_style
|
|
1955
|
+
).ask()
|
|
1956
|
+
|
|
1957
|
+
if not framework:
|
|
1958
|
+
print_error("取消创建")
|
|
1959
|
+
raise SystemExit(0)
|
|
1960
|
+
|
|
1961
|
+
project_path = Path(project_name)
|
|
1962
|
+
|
|
1963
|
+
if project_path.exists():
|
|
1964
|
+
print_error(f"目录 '{project_name}' 已存在")
|
|
1965
|
+
raise SystemExit(1)
|
|
1966
|
+
|
|
1967
|
+
print_kv("创建项目", project_name)
|
|
1968
|
+
print_kv("框架", framework)
|
|
1969
|
+
|
|
1970
|
+
package_name = project_path.name.replace('-', '_')
|
|
1971
|
+
project_path.mkdir(parents=True)
|
|
1972
|
+
if framework not in {"openclaw", "hermes"}:
|
|
1973
|
+
(project_path / package_name).mkdir(parents=True)
|
|
1974
|
+
|
|
1975
|
+
# 检测全局配置
|
|
1976
|
+
from ksadk.configs.global_config import (
|
|
1977
|
+
global_config_exists,
|
|
1978
|
+
get_env_from_global_config,
|
|
1979
|
+
)
|
|
1980
|
+
|
|
1981
|
+
global_env = {}
|
|
1982
|
+
if global_config_exists():
|
|
1983
|
+
global_env = get_env_from_global_config()
|
|
1984
|
+
if global_env:
|
|
1985
|
+
print_info("检测到全局配置,已自动填充凭证")
|
|
1986
|
+
|
|
1987
|
+
# .env - 生成配置文件
|
|
1988
|
+
# 如果有全局配置,使用全局配置的值;否则使用占位符
|
|
1989
|
+
# 如果有全局配置,使用全局配置的值;否则使用空字符串
|
|
1990
|
+
api_key = global_env.get("OPENAI_API_KEY", "")
|
|
1991
|
+
base_url = global_env.get("OPENAI_BASE_URL", "")
|
|
1992
|
+
model_name = global_env.get("OPENAI_MODEL_NAME", "")
|
|
1993
|
+
|
|
1994
|
+
ks_ak = global_env.get("KSYUN_ACCESS_KEY", "")
|
|
1995
|
+
ks_sk = global_env.get("KSYUN_SECRET_KEY", "")
|
|
1996
|
+
ks_region = global_env.get("KSYUN_REGION", "cn-beijing-6")
|
|
1997
|
+
ks_account = global_env.get("KSYUN_ACCOUNT_ID", "")
|
|
1998
|
+
|
|
1999
|
+
# 构建 .env 内容
|
|
2000
|
+
if framework == "openclaw":
|
|
2001
|
+
env_content = f"""# ======================
|
|
2002
|
+
# OpenClaw 标准部署最小配置
|
|
2003
|
+
# ======================
|
|
2004
|
+
KSYUN_ACCESS_KEY={ks_ak}
|
|
2005
|
+
KSYUN_SECRET_KEY={ks_sk}
|
|
2006
|
+
KSYUN_REGION={ks_region}
|
|
2007
|
+
"""
|
|
2008
|
+
if ks_account:
|
|
2009
|
+
env_content += f"KSYUN_ACCOUNT_ID={ks_account}\n"
|
|
2010
|
+
else:
|
|
2011
|
+
env_content += "# KSYUN_ACCOUNT_ID=your-account-id\n"
|
|
2012
|
+
|
|
2013
|
+
env_content += f"\nOPENAI_API_KEY={api_key}\n"
|
|
2014
|
+
if base_url:
|
|
2015
|
+
env_content += f"OPENAI_BASE_URL={base_url}\n"
|
|
2016
|
+
else:
|
|
2017
|
+
env_content += "# OPENAI_BASE_URL=http://kspmas.ksyun.com/v1\n"
|
|
2018
|
+
|
|
2019
|
+
if model_name:
|
|
2020
|
+
env_content += f"OPENAI_MODEL_NAME={model_name}\n"
|
|
2021
|
+
else:
|
|
2022
|
+
env_content += "# OPENAI_MODEL_NAME=glm-5.2\n"
|
|
2023
|
+
elif framework == "hermes":
|
|
2024
|
+
env_content = f"""# ======================
|
|
2025
|
+
# Hermes 标准部署最小配置
|
|
2026
|
+
# ======================
|
|
2027
|
+
KSYUN_ACCESS_KEY={ks_ak}
|
|
2028
|
+
KSYUN_SECRET_KEY={ks_sk}
|
|
2029
|
+
KSYUN_REGION={ks_region}
|
|
2030
|
+
"""
|
|
2031
|
+
if ks_account:
|
|
2032
|
+
env_content += f"KSYUN_ACCOUNT_ID={ks_account}\n"
|
|
2033
|
+
else:
|
|
2034
|
+
env_content += "# KSYUN_ACCOUNT_ID=your-account-id\n"
|
|
2035
|
+
|
|
2036
|
+
env_content += f"""
|
|
2037
|
+
OPENAI_API_KEY={api_key}
|
|
2038
|
+
"""
|
|
2039
|
+
if base_url:
|
|
2040
|
+
env_content += f"OPENAI_BASE_URL={base_url}\n"
|
|
2041
|
+
else:
|
|
2042
|
+
env_content += "# OPENAI_BASE_URL=http://kspmas.ksyun.com/v1\n"
|
|
2043
|
+
|
|
2044
|
+
if model_name:
|
|
2045
|
+
env_content += f"OPENAI_MODEL_NAME={model_name}\n"
|
|
2046
|
+
else:
|
|
2047
|
+
env_content += "# OPENAI_MODEL_NAME=glm-5.2\n"
|
|
2048
|
+
|
|
2049
|
+
env_content += """
|
|
2050
|
+
# Hermes runtime
|
|
2051
|
+
API_SERVER_ENABLED=true
|
|
2052
|
+
API_SERVER_HOST=127.0.0.1
|
|
2053
|
+
API_SERVER_PORT=8642
|
|
2054
|
+
HERMES_DASHBOARD_HOST=127.0.0.1
|
|
2055
|
+
HERMES_DASHBOARD_PORT=9119
|
|
2056
|
+
PORT=8080
|
|
2057
|
+
# HERMES_CONTEXT_LENGTH=200000
|
|
2058
|
+
# HERMES_FALLBACK_MODEL=deepseek-v4-pro
|
|
2059
|
+
# HERMES_IMAGE=hub.kce.ksyun.com/agentengine-public/hermes-agent:2026.5.29.2-ksadk-v1
|
|
2060
|
+
"""
|
|
2061
|
+
env_example_content = """# ======================
|
|
2062
|
+
# Hermes 标准部署最小配置示例
|
|
2063
|
+
# ======================
|
|
2064
|
+
KSYUN_ACCESS_KEY=your-access-key
|
|
2065
|
+
KSYUN_SECRET_KEY=your-secret-key
|
|
2066
|
+
KSYUN_REGION=cn-beijing-6
|
|
2067
|
+
# KSYUN_ACCOUNT_ID=your-account-id
|
|
2068
|
+
|
|
2069
|
+
OPENAI_API_KEY=your-model-api-key
|
|
2070
|
+
OPENAI_BASE_URL=https://kspmas.ksyun.com/v1/
|
|
2071
|
+
OPENAI_MODEL_NAME=glm-5.2
|
|
2072
|
+
|
|
2073
|
+
# Hermes runtime
|
|
2074
|
+
API_SERVER_ENABLED=true
|
|
2075
|
+
API_SERVER_HOST=127.0.0.1
|
|
2076
|
+
API_SERVER_PORT=8642
|
|
2077
|
+
HERMES_DASHBOARD_HOST=127.0.0.1
|
|
2078
|
+
HERMES_DASHBOARD_PORT=9119
|
|
2079
|
+
PORT=8080
|
|
2080
|
+
# HERMES_CONTEXT_LENGTH=200000
|
|
2081
|
+
# HERMES_FALLBACK_MODEL=deepseek-v4-pro
|
|
2082
|
+
# HERMES_IMAGE=hub.kce.ksyun.com/agentengine-public/hermes-agent:2026.5.29.2-ksadk-v1
|
|
2083
|
+
"""
|
|
2084
|
+
else:
|
|
2085
|
+
langfuse_public = global_env.get("LANGFUSE_PUBLIC_KEY", "")
|
|
2086
|
+
langfuse_secret = global_env.get("LANGFUSE_SECRET_KEY", "")
|
|
2087
|
+
langfuse_url = global_env.get("LANGFUSE_BASE_URL", "")
|
|
2088
|
+
|
|
2089
|
+
env_content = f"""# ======================
|
|
2090
|
+
# 模型配置 (必填, 可以从星流平台获取https://ksp.console.ksyun.com/#/apiKey)
|
|
2091
|
+
# ======================
|
|
2092
|
+
OPENAI_API_KEY={api_key}
|
|
2093
|
+
"""
|
|
2094
|
+
|
|
2095
|
+
# 可选字段:如果有值则启用,否则注释掉
|
|
2096
|
+
if base_url:
|
|
2097
|
+
env_content += f"OPENAI_BASE_URL={base_url}\n"
|
|
2098
|
+
else:
|
|
2099
|
+
env_content += "# OPENAI_BASE_URL=http://kspmas.ksyun.com/v1\n"
|
|
2100
|
+
|
|
2101
|
+
if model_name:
|
|
2102
|
+
env_content += f"OPENAI_MODEL_NAME={model_name}\n"
|
|
2103
|
+
else:
|
|
2104
|
+
env_content += "# OPENAI_MODEL_NAME=glm-5.2\n"
|
|
2105
|
+
|
|
2106
|
+
env_content += """
|
|
2107
|
+
# ======================
|
|
2108
|
+
# 可观测性 (可选)
|
|
2109
|
+
# ======================
|
|
2110
|
+
"""
|
|
2111
|
+
if langfuse_public:
|
|
2112
|
+
env_content += f"LANGFUSE_PUBLIC_KEY={langfuse_public}\n"
|
|
2113
|
+
else:
|
|
2114
|
+
env_content += "# LANGFUSE_PUBLIC_KEY=pk-xxx\n"
|
|
2115
|
+
|
|
2116
|
+
if langfuse_secret:
|
|
2117
|
+
env_content += f"LANGFUSE_SECRET_KEY={langfuse_secret}\n"
|
|
2118
|
+
else:
|
|
2119
|
+
env_content += "# LANGFUSE_SECRET_KEY=sk-xxx\n"
|
|
2120
|
+
|
|
2121
|
+
if langfuse_url:
|
|
2122
|
+
env_content += f"LANGFUSE_BASE_URL={langfuse_url}\n"
|
|
2123
|
+
else:
|
|
2124
|
+
env_content += "# LANGFUSE_BASE_URL=https://cloud.langfuse.com\n"
|
|
2125
|
+
|
|
2126
|
+
env_content += """
|
|
2127
|
+
# ======================
|
|
2128
|
+
# 金山云配置 (可选,需要部署时必选)
|
|
2129
|
+
# ======================
|
|
2130
|
+
"""
|
|
2131
|
+
if ks_ak:
|
|
2132
|
+
env_content += f"KSYUN_ACCESS_KEY={ks_ak}\n"
|
|
2133
|
+
else:
|
|
2134
|
+
env_content += "# KSYUN_ACCESS_KEY=your-api-key-here\n"
|
|
2135
|
+
|
|
2136
|
+
if ks_sk:
|
|
2137
|
+
env_content += f"KSYUN_SECRET_KEY={ks_sk}\n"
|
|
2138
|
+
else:
|
|
2139
|
+
env_content += "# KSYUN_SECRET_KEY=your-api-secret-here\n"
|
|
2140
|
+
|
|
2141
|
+
env_content += f"KSYUN_REGION={ks_region}\n"
|
|
2142
|
+
|
|
2143
|
+
if ks_account:
|
|
2144
|
+
env_content += f"KSYUN_ACCOUNT_ID={ks_account}\n"
|
|
2145
|
+
else:
|
|
2146
|
+
env_content += "# KSYUN_ACCOUNT_ID=your-account-id\n"
|
|
2147
|
+
|
|
2148
|
+
# 使用 utf-8-sig 编码 (带 BOM),确保 Windows 程序正确识别为 UTF-8
|
|
2149
|
+
(project_path / ".env").write_text(env_content, encoding="utf-8-sig")
|
|
2150
|
+
if framework == "hermes":
|
|
2151
|
+
(project_path / ".env.example").write_text(env_example_content, encoding="utf-8-sig")
|
|
2152
|
+
|
|
2153
|
+
if framework == "openclaw":
|
|
2154
|
+
print_success("项目创建成功")
|
|
2155
|
+
print_rule("快速开始")
|
|
2156
|
+
print_info("快速开始 (复制并执行):")
|
|
2157
|
+
_print_quick_start_commands(project_name, ["agentengine openclaw deploy"])
|
|
2158
|
+
print_info("部署前如需覆盖模型/网关参数,可先编辑 .env")
|
|
2159
|
+
return
|
|
2160
|
+
if framework == "hermes":
|
|
2161
|
+
_write_hermes_project_template(project_path, project_name, package_name)
|
|
2162
|
+
print_success("项目创建成功")
|
|
2163
|
+
print_rule("快速开始")
|
|
2164
|
+
print_info("快速开始 (复制并执行):")
|
|
2165
|
+
_print_quick_start_commands(project_name, ["agentengine hermes deploy"])
|
|
2166
|
+
print_info("部署前如需覆盖模型/运行时参数,可先编辑 .env")
|
|
2167
|
+
return
|
|
2168
|
+
|
|
2169
|
+
# agentengine.yaml - Agent 配置
|
|
2170
|
+
(project_path / "agentengine.yaml").write_text(f"""# AgentEngine 项目配置
|
|
2171
|
+
name: {package_name}
|
|
2172
|
+
version: "1.0.0"
|
|
2173
|
+
|
|
2174
|
+
# 框架类型: adk, langchain, langgraph, deepagents
|
|
2175
|
+
framework: {framework}
|
|
2176
|
+
|
|
2177
|
+
# Agent 入口
|
|
2178
|
+
entry_point: {package_name}/agent.py
|
|
2179
|
+
agent_variable: root_agent
|
|
2180
|
+
|
|
2181
|
+
# 部署配置 (可选)
|
|
2182
|
+
# deploy:
|
|
2183
|
+
# timeout: 300
|
|
2184
|
+
# memory: 512
|
|
2185
|
+
""", encoding="utf-8-sig")
|
|
2186
|
+
|
|
2187
|
+
# __init__.py
|
|
2188
|
+
(project_path / package_name / "__init__.py").write_text(f'''"""
|
|
2189
|
+
{project_name} - KsADK Agent
|
|
2190
|
+
"""
|
|
2191
|
+
from .agent import root_agent
|
|
2192
|
+
__all__ = ["root_agent"]
|
|
2193
|
+
''', encoding="utf-8-sig")
|
|
2194
|
+
|
|
2195
|
+
# agent.py
|
|
2196
|
+
template = TEMPLATES[framework]["agent.py"]
|
|
2197
|
+
(project_path / package_name / "agent.py").write_text(
|
|
2198
|
+
template.format(package_name=package_name),
|
|
2199
|
+
encoding="utf-8-sig"
|
|
2200
|
+
)
|
|
2201
|
+
|
|
2202
|
+
# README.md
|
|
2203
|
+
if framework == "openclaw":
|
|
2204
|
+
readme = f"""# {project_name}
|
|
2205
|
+
|
|
2206
|
+
基于 AgentEngine 创建的 OpenClaw 项目。
|
|
2207
|
+
|
|
2208
|
+
## 快速开始
|
|
2209
|
+
|
|
2210
|
+
```bash
|
|
2211
|
+
cd {project_name}
|
|
2212
|
+
|
|
2213
|
+
# 1. 编辑 .env(可选,覆盖模型/网关参数)
|
|
2214
|
+
vim .env
|
|
2215
|
+
|
|
2216
|
+
# 2. 部署 OpenClaw(默认 trusted-proxy)
|
|
2217
|
+
agentengine openclaw deploy
|
|
2218
|
+
|
|
2219
|
+
# 3. 打开 Dashboard
|
|
2220
|
+
agentengine dashboard --share
|
|
2221
|
+
```
|
|
2222
|
+
"""
|
|
2223
|
+
else:
|
|
2224
|
+
readme = f"""# {project_name}
|
|
2225
|
+
|
|
2226
|
+
基于 AgentEngine 创建的 {framework.upper()} Agent.
|
|
2227
|
+
|
|
2228
|
+
## 快速开始
|
|
2229
|
+
|
|
2230
|
+
```bash
|
|
2231
|
+
cd {project_name}
|
|
2232
|
+
|
|
2233
|
+
# 1. 编辑 .env 填写 API Key
|
|
2234
|
+
vim .env
|
|
2235
|
+
|
|
2236
|
+
# 2. 运行
|
|
2237
|
+
agentengine run -i . # 交互式
|
|
2238
|
+
agentengine web . # API Server
|
|
2239
|
+
agentengine deploy . # 部署到云端
|
|
2240
|
+
```
|
|
2241
|
+
|
|
2242
|
+
## 项目结构
|
|
2243
|
+
|
|
2244
|
+
```
|
|
2245
|
+
{project_name}/
|
|
2246
|
+
├── .env # 环境变量 (API Key 等)
|
|
2247
|
+
├── agentengine.yaml # Agent 配置
|
|
2248
|
+
├── requirements.txt # Python 依赖
|
|
2249
|
+
├── {package_name}/
|
|
2250
|
+
│ ├── __init__.py
|
|
2251
|
+
│ └── agent.py # Agent 实现
|
|
2252
|
+
└── README.md
|
|
2253
|
+
```
|
|
2254
|
+
"""
|
|
2255
|
+
|
|
2256
|
+
(project_path / "README.md").write_text(readme, encoding="utf-8-sig")
|
|
2257
|
+
|
|
2258
|
+
# requirements.txt
|
|
2259
|
+
reqs = "requests_aws4auth\n" # Minimum required for ksadk.common.auth
|
|
2260
|
+
if framework == "langchain":
|
|
2261
|
+
reqs += "langchain\nlangchain-openai\npython-dotenv\n"
|
|
2262
|
+
elif framework == "langgraph":
|
|
2263
|
+
reqs += "langchain\nlangchain-openai\nlanggraph\npython-dotenv\n"
|
|
2264
|
+
elif framework == "deepagents":
|
|
2265
|
+
reqs += "deepagents\nlangchain\nlangchain-openai\nlanggraph\npython-dotenv\n"
|
|
2266
|
+
elif framework == "adk":
|
|
2267
|
+
reqs += "google-adk\npython-dotenv\n"
|
|
2268
|
+
|
|
2269
|
+
(project_path / "requirements.txt").write_text(reqs, encoding="utf-8")
|
|
2270
|
+
|
|
2271
|
+
print_success("项目创建成功")
|
|
2272
|
+
print_rule("快速开始")
|
|
2273
|
+
|
|
2274
|
+
print_info("快速开始 (复制并执行):")
|
|
2275
|
+
_print_quick_start_commands(project_name, ["agentengine config"])
|
|
2276
|
+
print_info("或直接运行 (环境变量中需包含模型 API Key):")
|
|
2277
|
+
_print_quick_start_commands(project_name, ["agentengine run -i ."])
|