feat(dsl,nodes): support agent_strategy plugin invocation#135
Open
BenjaminX wants to merge 4 commits into
Open
feat(dsl,nodes): support agent_strategy plugin invocation#135BenjaminX wants to merge 4 commits into
BenjaminX wants to merge 4 commits into
Conversation
Add a first-class workflow node for Dify ``agent_strategy`` plugins
plus the slim-backed runtime adapter that drives them. This unlocks
Dify Studio chatflows that contain ``type: agent`` nodes —
``graphon.dsl.loads`` now accepts and executes them end-to-end.
Three layers, all shipped together so the change is atomic:
1. **Slim layer** (``src/graphon/dsl/slim/agent.py``):
- ``SlimAgentStrategyClient`` — thin invoker scoped by
``(plugin_id, agent_strategy_provider, agent_strategy)``. Mirrors
``SlimLLM`` and ``SlimToolNodeRuntime``: ``action_invoker``
injection for tests, ``SlimClient`` for subprocess. Errors from
``action_invoker`` propagate unchanged (caller-owned); errors
from the SlimClient path are wrapped into ``SlimAgentStrategyError``
(library-owned boundary).
- ``AgentRuntimeMessage`` — PEP 695 alias of ``ToolRuntimeMessage``
because the Dify Plugin SDK defines
``AgentInvokeMessage(InvokeMessage): pass`` without adding fields.
The alias keeps call sites semantically clear and reserves room for
divergence later.
- ``SlimActionInvoker`` — DI seam.
- ``_decode_agent_message`` — dedicated decoder for the agent message
subset (text / link / json / log / variable / retriever_resources).
Independent from the tool runtime's decoder so the agent path is
not coupled to tool-specific variants (file / blob / image).
2. **Node layer** (``src/graphon/nodes/agent/``):
- ``AgentNodeData`` mirrors the Dify Studio v1.7+ export shape — the
``agent_strategy_provider_name`` / ``agent_strategy_name`` /
``plugin_unique_identifier`` triple plus the ``agent_parameters``
typed-wrapper bag.
- ``AgentParameterValue`` is the ``{type, value}`` wrapper Studio
emits for every parameter (constant / variable / mixed),
type-checked at validation time.
- ``AgentNode._run`` is a streaming generator that resolves
typed-wrapper parameters against the variable pool, forwards them
through the injected runtime, translates each runtime message into
the matching graph event — text/link → ``StreamChunkEvent``
selected by ``[node_id, "text"]``, log → ``AgentLogEvent``, json
/ variable → accumulated outputs — and emits one terminal
``StreamCompletedEvent``.
- ``AgentNodeError`` is the node-layer boundary error type.
- ``AgentNodeRuntimeProtocol`` (in ``nodes/runtime.py``) decouples
the node from any specific runtime.
3. **DSL wiring** (``src/graphon/dsl/``):
- ``SlimAgentNodeRuntime`` (``agent_runtime.py``) implements
``AgentNodeRuntimeProtocol`` by assembling a fresh
``SlimAgentStrategyClient`` per invocation. Stateless adapter that
mirrors how ``SlimDslNodeFactory`` constructs LLM and tool slim
clients per node. ``_provider_slug`` extracts the trailing segment
of ``agent_strategy_provider_name`` (e.g. "langgenius/agent/agent"
→ "agent") to match the slim payload contract.
- ``SlimDslNodeFactory.create_node`` routes ``BuiltinNodeTypes.AGENT``
to ``AgentNode`` with a slim-backed runtime built from the
factory's existing ``slim_client_config``.
- ``_SUPPORTED_DEFAULT_FACTORY_NODES`` in the importer now includes
``BuiltinNodeTypes.AGENT``.
Tests: 28 new cases across three files cover the slim adapter
(``tests/dsl/test_slim_agent.py``: 12 cases — basic / mixed-type
decoding, payload contract, meta propagation, lazy partial consumption,
both error-path contracts, alias identity), the node behavior
(``tests/nodes/agent/test_agent_node.py``: 10 cases — text streaming,
log → ``AgentLogEvent``, json / variable accumulation, all three
typed-wrapper resolution modes, runtime-error wrap), the slim runtime
adapter (``tests/dsl/test_slim_agent_node_runtime.py``: 4 cases — slug
extraction, payload forwarding, decoded message contract, per-node
client identity), and factory routing
(``tests/dsl/test_node_factory_agent.py``: 2 cases).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eaming Standard downstream-integration pattern for executing Dify Studio exported chatflow / workflow DSLs through ``graphon.dsl.loads``. What ``main.py`` demonstrates end-to-end: - Static DSL inspection via ``graphon.dsl.inspect`` before execution, printing document kind, plugin dependencies, and load status. Aborts early with a readable diagnostic for non-loadable plans (unsupported node types, config-only ``app.mode`` like ``chat`` / ``completion`` / ``agent-chat``, unresolvable plugin dependencies). - Execution via ``graphon.dsl.loads`` — the canonical 4-line integration surface; everything else in main.py is decoration. - Live event consumption: ``NodeRunStreamChunkEvent`` writes chunks to stdout as they arrive, ``NodeRunStartedEvent`` shows per-node lifecycle, ``NodeRunAgentLogEvent`` exposes agent strategy inner steps, ``GraphRunSucceededEvent`` collects the final ``outputs["answer"]``. - Credential isolation: keys live only in ``credentials.json`` (gitignored via the repo-wide ``examples/*/credentials.json`` rule). No ambient environment variables are consulted for secrets. - Slim binary auto-discovery: ``SLIM_BINARY_PATH`` env var wins, then a local ``./slim`` file in the example directory, otherwise rely on ``PATH``. Includes ``credentials.example.json`` matching the upstream ``examples/slim_llm`` convention so credential plumbing is consistent across demos. Multi-vendor template lists both ``tongyi`` (DashScope) and ``openai`` (with custom ``openai_api_base`` + ``api_protocol`` fields exposed by the ``langgenius/openai`` plugin). README is a standard usage reference for downstream integrators: observed event types, the 4-line integration core, supported / unsupported node types and modes, troubleshooting table. Also extends ``.gitignore`` to cover ``.scratch/`` (local design / review notes) and ``examples/*/credentials.json`` (per-example secrets, never committed). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous pin ``langgenius/openai:0.3.8@<digest>`` was rejected by the live marketplace with ``plugin package not found`` (500). The upstream ``examples/slim_llm`` demo therefore did not run end-to-end on a fresh ``.slim/plugins`` cache. Updating both the demo's ``graph.yml`` and ``settings.py`` to the current marketplace head ``0.4.0`` (digest ``beafb5a726eda839a1839f61a0456ae7e068c98624c53f59b07be9a71fbf72da``) restores marketplace download. No behavioral change in the demo itself. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
All contributors on this pull request have signed the CLA. |
Author
|
I have read the CLA Document and I hereby sign the CLA |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the two integration surfaces flagged in RFC #102 so external integrators can drive Dify chatflows containing agent nodes through
graphon.dsl.loads.dsl/slimlearns theinvoke_agent_strategyslim action via a newSlimAgentStrategyClient. ReusesToolRuntimeMessageas the wire DTO because the Dify Plugin SDK definesAgentInvokeMessage(InvokeMessage): pass— wire format is identical, the tool-side decoder is shared.AgentNode,AgentNodeRuntimeProtocol, andSlimAgentNodeRuntime; routesBuiltinNodeTypes.AGENTthroughSlimDslNodeFactory. Mirrors the structure ofToolNode/SlimToolNodeRuntime.examples/chatflow_dsl_runner, a canonical downstream-integration pattern usinggraphon.dsl.loadsagainst a real Dify Studio chatflow export. Two fixtures bundled: one runs out of the box, one documents a known limitation (see below).examples/slim_llmfrom 0.3.8 to 0.4.0 to align with the new chatflow example.What's new
src/graphon/dsl/slim/agent.py—SlimAgentStrategyClient,SlimAgentStrategyError,AgentRuntimeMessagealias,SlimActionInvokerDI seam.src/graphon/dsl/agent_runtime.py—SlimAgentNodeRuntime(AgentNodeRuntimeProtocol), mirrorsSlimToolNodeRuntime.src/graphon/dsl/_provider.py— extracted sharedcanonical_vendorhelper (deduped fromnode_factory.py).src/graphon/nodes/agent/{agent_node.py, entities.py, exc.py}—AgentNode,AgentNodeData(typed-wrapperagent_parameters),AgentParameterValue,AgentNodeError.src/graphon/nodes/runtime.py— newAgentNodeRuntimeProtocol.src/graphon/dsl/{node_factory.py, importer.py}— routeBuiltinNodeTypes.AGENT; add to_SUPPORTED_DEFAULT_FACTORY_NODES.examples/chatflow_dsl_runner/—main.py(CLI withinspect()/loads()/ event streaming),chatflow_dsl_simple.yml(runnable),chatflow_dsl_agent.yml(reference fixture, see limitations),README.md,credentials.example.json.Out of scope (deliberate)
tenant_id/user_idare intentionally not propagated through the slim runtime. When an agent strategy plugin doesself.session.model.llm.invoke(...), the daemon currently performs backwards-invocation against the Dify Server inner API at:5001, which requires a tenant context. Teaching graphon to forwardtenant_id/user_idwould leak Dify's business-side identity model into a tenant-agnostic execution engine.The correct long-term fix is the daemon redesign outlined in the RFC #102 "long-term direction" — let plugin code perform nested invocations without depending on Dify Server's inner API. Until that lands,
chatflow_dsl_agent.ymlships as a structural reference (this is what a real Dify Studio export of a chatflow with an agent looks like) but cannot run end-to-end from a graphon-only runner.chatflow_dsl_simple.ymlis the runnable out-of-box fixture.This trade-off is documented in
examples/chatflow_dsl_runner/README.md#architecture--limitations.Test plan
pytest tests/— 348 passed (28 new tests acrosstest_slim_agent.py,test_agent_node.py,test_slim_agent_node_runtime.py,test_node_factory_agent.py).make tc— ruff format + check + ty all green.python3 examples/chatflow_dsl_runner/main.py chatflow_dsl_simple.yml "Say hi in 5 words."streams a real LLM response fromlanggenius/openai:0.4.0.action_invokerinjection seam — no e2e slim-binary tests on CI by design.Open questions for reviewers
agent_strategyandtoolplugin invocation #102 originally split the work into PR-A (slim) and PR-B (node). They're bundled here because PR-B is hard to verify without PR-A and the example is the smallest end-to-end witness we have. Happy to split into stacked PRs if preferred — the three commits (984a416slim+node,23855bdexample,f439196slim_llm bump) are natural boundaries.AgentRuntimeMessage = ToolRuntimeMessagealias — keeps a semantic name so future agent-only message variants can diverge, while structurally reusing the tool DTO and decoder. Alternative is to inlineToolRuntimeMessageat the agent call sites. The reasoning is in a comment atsrc/graphon/dsl/slim/agent.py:15.AgentNode.version() → "1"— matches thestart/llm/toolconvention. Distinct fromtool_node_version: "2"inAgentNodeData, which is the Dify plugin protocol version forwarded verbatim to the strategy plugin.chatflow_dsl_agent.ymlplacement — kept underexamples/with a README disclaimer rather thantests/fixtures/because it doubles as documentation for the architecture limitation. Open to relocation ifexamples/is meant to be strictly run-out-of-box.slim_llmopenai bump (f439196) — kept as a separate commit; trivially droppable from this PR if maintainers prefer it as a follow-up chore.References
agent_strategyandtoolplugin invocation #102